/* Parta web — hand-written CSS, no build step.
 *
 * Everything themeable is a custom property on :root. The palette is the only
 * place to touch when the brand assets land; nothing below hard-codes a colour.
 *
 * Written mobile-first: the base rules target a 375px phone, and the media
 * queries only ever add. Roughly all of this audience is on a phone.
 */

:root {
  /* Surfaces, darkest to lightest. */
  --bg:            #0a0a0c;
  --surface:       #131317;
  --surface-2:     #1b1b21;
  --surface-3:     #24242c;
  --line:          #2a2a33;
  --line-strong:   #3a3a46;

  /* Text. */
  --text:          #f2f2f5;
  --text-muted:    #a1a1ad;
  --text-faint:    #6e6e7c;

  /* Accent. Swap these two for the brand colours.
   *
   * Azure into deep cyan, by owner decision — a blue family, close enough to
   * the category leader to feel native and far enough from its flat #0a84ff
   * to be our own. Both stops are chosen, not picked by eye: --accent-ink is
   * white and sits on top of them, so each end has to clear 3:1 against white
   * (4.00 and 3.62 here). Every brighter cyan that reads as "голубой" —
   * #22dfd0, #19c6c6, #0fa8d4 — lands between 1.7 and 2.8 and turns every
   * button label into grey mush. If these are ever changed, check the pair
   * again before shipping. */
  --accent:        #1c7fe8;
  --accent-2:      #0d93ad;
  --accent-ink:    #ffffff;

  /* The warm accent, by owner decision. It is not a second brand colour — it
   * is the one reserved for offers and deadlines, so a discount never has to
   * borrow the blue that means "this is the product" and can be told apart
   * from it at a glance. */
  --accent-warm:   #dc9546;
  /* The bright cyan does have a place: on the near-black background it is
   * high-contrast and unmissable, so it carries badges and highlights. */
  --accent-bright: #35d6e8;
  /* The paid mark. Purple because the whole palette is blue-cyan and this has
     to read as "different kind of thing", not "brighter button". Both stops
     checked against white at 12px/700: 7.1:1 and 5.4:1, so the text passes AA
     across the whole sweep rather than only at the dark end. */
  --paid:          #4c1d95;
  --paid-2:        #6d28d9;

  --ok:            #35c67a;
  --danger:        #ff5b5b;

  /* Rounder, by owner decision. The scale keeps its steps — small things stay
   * proportionally less round than large ones, or a 26px logo tile with a 20px
   * radius stops being a square and becomes a blob. */
  --radius:        18px;
  --radius-md:     14px;
  --radius-lg:     26px;
  --radius-pill:   999px;

  --shadow:        0 1px 2px rgb(0 0 0 / .4), 0 8px 24px rgb(0 0 0 / .35);
  --shadow-lift:   0 2px 4px rgb(0 0 0 / .45), 0 16px 40px rgb(0 0 0 / .5);

  --wrap:          1240px;
  --wrap-narrow:   720px;

  /* Taller by owner decision. Only the bar grows — the controls inside keep
     their own sizes, so this buys air around them rather than bigger buttons. */
  --header-h:      72px;

  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}

/* ── Reset ───────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchors land below the sticky header instead of underneath it. */
  scroll-padding-top: calc(var(--header-h) + 12px);
}

body {
  margin: 0;
  /* A column so the footer can be pushed down. Without this, a short page —
     a 404, an empty gallery, the admin login — left the footer floating in
     the middle of the viewport with a band of nothing under it. */
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
}

/* The age gate is a real barrier, not decoration: while it is up the page
 * behind it does not scroll. */
body.is-gated { overflow: hidden; }

h1, h2, h3 { line-height: 1.2; margin: 0; font-weight: 650; letter-spacing: -0.02em; }
p  { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }

/* Inline icons take the current text colour.
 *
 * An SVG shape with no fill attribute defaults to BLACK, and every surface
 * here is near-black — so a new icon macro that forgets `fill` is invisible,
 * and nothing about the markup says so. Four of them had already gone that
 * way. This makes the default correct.
 *
 * :where() is load-bearing, not decoration. Written plainly, `svg[class^="i-"]`
 * is specificity (0,1,1) — the element counts — which BEATS a plain `.i-credit`
 * (0,1,0), so a default silently overrode every icon that had chosen its own
 * colour and turned the credit star white. :where() contributes zero, so this
 * is a floor that any class clears. */
:where(svg[class^="i-"], svg[class*=" i-"]) { fill: currentColor; }
img, video { max-width: 100%; display: block; }

a { color: inherit; text-decoration: none; }
a:hover { color: var(--text); }

/* Any author `display` beats the UA stylesheet's rule for [hidden], so an
 * element with display:flex stays visible while the markup says it is hidden.
 * This makes the attribute mean what it says, everywhere. */
[hidden] { display: none !important; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 6px;
}

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

.skip-link {
  position: absolute; inset-inline-start: -9999px; top: 0; z-index: 100;
  background: var(--accent); color: var(--accent-ink);
  padding: 10px 16px; border-radius: 0 0 var(--radius) 0;
}
.skip-link:focus { inset-inline-start: 0; }

/* ── Layout ──────────────────────────────────────────────────────────────── */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: 16px;
}
.wrap-narrow { max-width: var(--wrap-narrow); }

.section { padding-block: 40px; }
.section-alt { background: var(--surface); border-block: 1px solid var(--line); }

.section-head { margin-bottom: 20px; }
.section-title { font-size: 24px; }
.section-sub { color: var(--text-muted); margin-top: 6px; font-size: 15px; }

.page-title { font-size: 26px; margin-bottom: 10px; }
/* For a page whose content is centred under it. Not the default: a list wants
   its heading where the list starts. */
.page-title-centre { text-align: center; }
.lede { color: var(--text-muted); font-size: 16px; }
.lede-note { color: var(--text-faint); font-size: 14px; margin-top: 8px; }

/* ── Header ──────────────────────────────────────────────────────────────── */

.site-header {
  position: sticky; top: 0; z-index: 30;
  height: var(--header-h);
  display: flex; align-items: center;
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  backdrop-filter: saturate(160%) blur(12px);
  border-bottom: 1px solid var(--line);
}

.header-inner {
  display: flex; align-items: center; gap: 12px;
}

.brand { display: flex; align-items: center; gap: 9px; margin-inline-end: auto; }
.brand-mark {
  width: 26px; height: 26px; border-radius: 13px;
  background: linear-gradient(140deg, var(--accent), var(--accent-2));
  box-shadow: 0 0 0 1px rgb(255 255 255 / .08) inset;
  flex: none;
}
.brand-name { font-weight: 700; letter-spacing: -0.02em; font-size: 17px; }

/* Nav links are noise on a phone — the same two anchors sit in the page. */
.header-nav { display: none; gap: 20px; font-size: 15px; color: var(--text-muted); }
.header-actions { display: flex; align-items: center; gap: 8px; }

.lang-form { display: flex; gap: 6px; }
.lang-select {
  appearance: none;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 6px 12px;
  font: inherit; font-size: 14px;
  cursor: pointer;
}

/* ── Buttons ─────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  padding: 12px 22px;
  font: inherit; font-weight: 600; font-size: 15px;
  cursor: pointer;
  transition: transform .08s ease, filter .15s ease, background-color .15s ease;
  /* Comfortably above the 44px touch target minimum. */
  min-height: 46px;
}
.btn:active { transform: translateY(1px); }

.btn-primary {
  /* Top to bottom, dark end at the bottom.
   *
   * --accent is the darker of the two (luminance 0.213 against 0.240), so it
   * goes last. Vertical rather than the old 135deg diagonal: a diagonal ramp
   * on a wide pill puts the light end in one corner, which reads as a
   * highlight coming from nowhere. Light above, dark below is where a surface
   * lit from above actually is. */
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
  color: var(--accent-ink);
  /* One soft layer, not --shadow.
   *
   * --shadow is two layers, and its first is a 1px contact shadow at 40%
   * black. On a rectangle that reads as the edge sitting on the page; on a
   * pill it traces the bottom curve and shows up as a hard dark line under the
   * button, much darker than the ambient shadow around it.
   *
   * Tinted with the accent rather than black: a coloured button casting a
   * neutral grey shadow looks pasted on. */
  box-shadow: 0 8px 22px color-mix(in srgb, var(--accent) 34%, transparent);
}
.btn-primary:hover { filter: brightness(1.08); color: var(--accent-ink); }

.btn-ghost {
  background: var(--surface-2);
  border-color: var(--line-strong);
  color: var(--text);
}
.btn-ghost:hover { background: var(--surface-3); }

.btn-sm { padding: 8px 16px; min-height: 38px; font-size: 14px; }
.btn-block { width: 100%; }

.btn.is-disabled,
.btn[disabled] { opacity: .55; cursor: not-allowed; pointer-events: none; }

.backlink {
  display: inline-block; color: var(--text-muted);
  font-size: 14px; margin-bottom: 16px;
}
.backlink::before { content: "← "; }

/* ── Hero ────────────────────────────────────────────────────────────────── */

.hero {
  padding-block: 44px 36px;
  background:
    radial-gradient(90% 120% at 50% -20%,
      color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
    var(--bg);
  border-bottom: 1px solid var(--line);
}
.hero-inner { text-align: center; }

.hero-badge {
  display: inline-block;
  font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-muted);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 5px 12px;
  margin-bottom: 16px;
}
.hero-title { font-size: 32px; letter-spacing: -0.03em; }
.hero-sub {
  color: var(--text-muted); margin-top: 12px; font-size: 16px;
  max-width: 46ch; margin-inline: auto;
}
.hero-actions {
  display: flex; flex-direction: column; gap: 10px;
  margin-top: 22px;
}
.hero-note { color: var(--text-faint); font-size: 13px; margin-top: 12px; }

/* ── Chips ───────────────────────────────────────────────────────────────── */

.chips {
  display: flex; gap: 8px;
  margin-bottom: 18px;
  /* Wide filter rows scroll inside themselves; the page never scrolls
   * sideways. */
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
}
.chips::-webkit-scrollbar { display: none; }

.chip {
  flex: none;
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--text-muted);
  border-radius: var(--radius-pill);
  padding: 8px 16px;
  font-size: 14px; font-weight: 550;
  white-space: nowrap;
}
.chip:hover { background: var(--surface-3); color: var(--text); }
.chip-on {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}
.chip-on:hover { background: var(--text); color: var(--bg); }

/* The catalogue grid is defined once, at the end of this file. */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color .15s ease, transform .15s ease, box-shadow .15s ease;
}
.card:hover {
  border-color: var(--line-strong);
  transform: translateY(-2px);
  box-shadow: var(--shadow-lift);
}
.card-free { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }

/* No height:100% here. In the catalogue the whole body sits inside this link,
 * so it fills the card on its own; in the gallery the caption and download
 * link sit OUTSIDE it, and a stretched link covered them both. */
.card-link { display: block; }

.card-media {
  /* One declaration, deliberately. This was spread over three, in three
     places, all saying position/overflow — and the later ones did nothing
     except make the file look like the earlier one was wrong. */
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 4;
  background: var(--surface-2);
  overflow: hidden;
}
.card-video, .card-img {
  width: 100%; height: 100%; object-fit: cover;
}

.card-clip { position: absolute; inset: 0; }
.card-placeholder {
  width: 100%; height: 100%;
  display: grid; place-items: center;
  color: var(--text-faint); font-size: 13px;
  background: repeating-linear-gradient(
    45deg, var(--surface-2), var(--surface-2) 10px, var(--surface) 10px, var(--surface) 20px);
}


.card-source-img {
  width: 100%; aspect-ratio: 3 / 4; object-fit: cover; display: block;
}
.card-source-tag {
  position: absolute; inset: auto 0 0 0;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  color: var(--text);
  font-size: 9px; font-weight: 700;
  text-align: center;
  padding: 2px 2px 3px;
  /* Long translations must not wrap this into two lines and cover the face. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.card-badges {
  position: absolute; inset-inline-start: 8px; top: 8px;
  display: flex; gap: 6px; flex-wrap: wrap;
}
.badge {
  font-size: 11px; font-weight: 700; letter-spacing: .03em;
  padding: 4px 9px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: blur(6px);
  color: var(--text);
  border: 1px solid rgb(255 255 255 / .1);
}
.badge-free {
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: var(--accent-ink); border-color: transparent;
}
.badge-muted { color: var(--text-muted); }

.card-body { padding: 10px 12px 14px; }

.card-desc {
  color: var(--text-muted); font-size: 13px; margin-top: 4px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.card-cta {
  display: inline-block; margin-top: 10px;
  font-size: 13px; font-weight: 650;
  color: var(--accent);
}
.card-cta::after { content: " →"; }

/* ── Empty state ─────────────────────────────────────────────────────────── */

.empty {
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-lg);
  padding: 40px 20px;
  text-align: center;
}
.empty-title { font-weight: 600; }
.empty-sub { color: var(--text-muted); font-size: 14px; margin: 6px 0 16px; }

/* ── Mode detail ─────────────────────────────────────────────────────────── */

.detail { display: grid; gap: 20px; }

/* ── Before → after pair ─────────────────────────────────────────────────── */

.pair {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 8px;
}
.pair-side { margin: 0; min-width: 0; }

.pair-frame {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--line-strong);
  background: var(--surface-2);
}
.pair-media {
  width: 100%; aspect-ratio: 3 / 4; object-fit: cover; display: block;
}
.pair-clip {
  position: absolute; inset: 0;
  height: 100%;
  opacity: 0;
  transition: opacity .25s ease;
}
.pair-clip.is-playing { opacity: 1; }
.pair-in .pair-frame  { border-style: dashed; }
.pair-out .pair-frame { border-color: color-mix(in srgb, var(--accent) 55%, var(--line-strong)); }

.pair-label {
  position: absolute; inset-inline-start: 8px; top: 8px;
  background: color-mix(in srgb, var(--bg) 76%, transparent);
  backdrop-filter: blur(6px);
  border: 1px solid rgb(255 255 255 / .1);
  border-radius: var(--radius-pill);
  padding: 4px 10px;
  font-size: 11px; font-weight: 700; letter-spacing: .03em;
}
.pair-label-out {
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
  border-color: transparent;
}

.pair-arrow {
  display: grid; place-items: center;
  width: 30px; height: 30px;
  border-radius: 50%;
  background: var(--surface-3);
  color: var(--text-muted);
  flex: none;
}

.pair-note { margin-top: 10px; color: var(--text-faint); font-size: 13px; }

.detail-title { font-size: 26px; }
.detail-desc { color: var(--text-muted); margin-top: 8px; }

.bullets { margin-top: 16px; display: grid; gap: 8px; }
.bullets li {
  color: var(--text-muted); font-size: 14px;
  padding-inline-start: 18px; position: relative;
}
.bullets li::before {
  content: ""; position: absolute; inset-inline-start: 0; top: 9px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent);
}

.variants { margin-top: 22px; display: grid; gap: 10px; }
.variants-head {
  font-size: 12px; font-weight: 650; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-faint);
}
.variant {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px 14px;
}
.variant-info { display: flex; align-items: baseline; gap: 8px; }
.variant-label { font-weight: 600; }
.variant-dur { color: var(--text-faint); font-size: 13px; }
.variant-meta { display: flex; align-items: center; gap: 12px; margin-inline-start: auto; }
.variant-price { font-weight: 650; font-size: 14px; color: var(--text-muted); white-space: nowrap; }

.hint { margin-top: 16px; color: var(--text-faint); font-size: 13px; }

/* ── Pricing ─────────────────────────────────────────────────────────────── */

.packs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.pack {
  position: relative;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px 14px;
  text-align: center;
}
.pack-featured {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent) inset;
}
.pack-flag {
  position: absolute; top: -9px; left: 50%; transform: translateX(-50%);
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
  font-size: 10px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  padding: 3px 10px; border-radius: var(--radius-pill);
  white-space: nowrap;
}
.pack-credits { font-size: 19px; font-weight: 700; }
.pack-cost { color: var(--text-muted); font-size: 14px; margin-top: 2px; }
.pack-save { color: var(--ok); font-size: 12px; font-weight: 650; margin-top: 6px; }

.costs {
  margin-top: 24px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 18px;
}
.costs-title { font-size: 15px; }
.costs-list { margin-top: 10px; display: grid; gap: 6px; color: var(--text-muted); font-size: 14px; }
.costs-note { margin-top: 14px; color: var(--text-faint); font-size: 13px; }
.costs-cta { margin-top: 16px; }

/* ── Forms ───────────────────────────────────────────────────────────────── */

.form { margin-top: 24px; display: grid; gap: 18px; }
.field { display: grid; gap: 6px; }
.label { font-size: 14px; font-weight: 600; }

.input {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  color: var(--text);
  /* 16px keeps iOS Safari from zooming the viewport on focus. */
  font: inherit; font-size: 16px;
  padding: 12px 14px;
}
.input::placeholder { color: var(--text-faint); }
.input:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); }
.textarea { resize: vertical; min-height: 130px; }

.field-invalid .input { border-color: var(--danger); }
.field-hint  { color: var(--text-faint); font-size: 13px; }
.field-hint a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.field-error { color: var(--danger); font-size: 13px; }

.alert {
  border-radius: var(--radius);
  padding: 12px 14px;
  font-size: 14px;
  margin-top: 20px;
}
.alert-error {
  background: color-mix(in srgb, var(--danger) 14%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--danger) 45%, transparent);
}

.notice {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 28px 20px;
  text-align: center;
  display: grid; gap: 10px; justify-items: center;
}
.notice .btn { margin-top: 8px; }
.notice-ok { border-color: color-mix(in srgb, var(--ok) 40%, var(--line)); }

.error-code {
  font-size: 44px; font-weight: 750; letter-spacing: -0.04em;
  color: var(--text-faint);
}

/* ── Prose (legal pages) ─────────────────────────────────────────────────── */

.prose h1 { font-size: 28px; margin-bottom: 6px; }
.prose h2 { font-size: 18px; margin-top: 30px; margin-bottom: 8px; }
.prose p, .prose li { color: var(--text-muted); font-size: 15px; }
.prose p + p { margin-top: 10px; }
.prose ul { margin: 10px 0; display: grid; gap: 8px; }
.prose ul li { padding-inline-start: 18px; position: relative; }
.prose ul li::before {
  content: ""; position: absolute; inset-inline-start: 2px; top: 10px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--text-faint);
}
.prose strong { color: var(--text); }
.prose a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.prose-meta { color: var(--text-faint); font-size: 13px; margin-bottom: 18px; }

/* ── Footer ──────────────────────────────────────────────────────────────── */

.site-footer {
  /* Pushed to the bottom on a short page, immediately after the content on a
     long one. margin-top rather than flex:1 on main, so a page that sets its
     own main height is unaffected. */
  margin-top: auto;
  border-top: 1px solid var(--line);
  padding-block: 28px 40px;
  background: var(--surface);
}
.footer-links {
  display: flex; flex-wrap: wrap; gap: 10px 20px;
  font-size: 14px; color: var(--text-muted);
}
.footer-note { color: var(--text-faint); font-size: 13px; margin-top: 16px; }

/* ── Age gate ────────────────────────────────────────────────────────────── */

.gate {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center;
  padding: 20px;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(14px);
  overflow-y: auto;
}
.gate-card {
  width: 100%; max-width: 420px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  padding: 26px 22px;
  box-shadow: var(--shadow-lift);
  text-align: center;
}
.gate-kicker {
  font-size: 12px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--text-faint);
}
.gate-title { font-size: 21px; margin-top: 6px; }
.gate-list { margin: 18px 0; display: grid; gap: 8px; text-align: start; }
.gate-list li {
  display: flex; gap: 10px; align-items: flex-start;
  background: var(--surface-2);
  border-radius: var(--radius);
  padding: 11px 13px;
  font-size: 14px; color: var(--text-muted);
}
.gate-icon {
  flex: none; min-width: 26px; height: 22px;
  display: grid; place-items: center;
  border-radius: 6px;
  background: var(--surface-3);
  color: var(--text); font-size: 11px; font-weight: 700;
}
.gate-note { color: var(--text-faint); font-size: 12px; margin-bottom: 16px; }
.gate-leave {
  display: inline-block; margin-top: 12px;
  color: var(--text-faint); font-size: 13px; text-decoration: underline;
}

/* ── Upload slot ─────────────────────────────────────────────────────────── */

.pair-drop { display: block; cursor: pointer; }
.pair-drop .pair-media { transition: opacity .2s ease, filter .2s ease; }
.pair-drop:hover .pair-media { opacity: .55; }

/* The example photo is dimmed under the call to action, so the panel reads as
 * "put yours here" rather than as a picture that happens to be clickable. */
.drop-cta {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  color: var(--text);
  text-align: center;
  padding: 10px;
}
.drop-plus {
  width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
  font-size: 22px; font-weight: 600; line-height: 1;
}
.drop-text { font-size: 12px; font-weight: 650; }

.pair-drop.has-file .drop-cta { background: color-mix(in srgb, var(--bg) 20%, transparent); }
.pair-drop.has-file .drop-text { font-size: 11px; }
.pair-drop.has-file .drop-plus { display: none; }

.consent-check { margin-top: 20px; }
.btn-generate { margin-top: 16px; }
.upload-hint { margin-top: 8px; text-align: center; }

.variant-pick { cursor: pointer; }
.variant-pick input {
  flex: none; width: 18px; height: 18px; accent-color: var(--accent);
}
.variant-pick:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
}

/* ── Job ─────────────────────────────────────────────────────────────────── */

.job { display: grid; gap: 20px; }
.job-frame {
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--line-strong);
  background: var(--surface);
}
.job-media { width: 100%; display: block; }
.job-pending {
  aspect-ratio: 3 / 4;
  display: grid; place-content: center; justify-items: center; gap: 12px;
  padding: 20px; text-align: center;
}
.job-state { font-weight: 650; }
.job-sub { color: var(--text-muted); font-size: 14px; }

.spinner {
  width: 34px; height: 34px;
  border-radius: 50%;
  border: 3px solid var(--surface-3);
  border-top-color: var(--accent);
  animation: spin 900ms linear infinite;
}
.spinner-sm { width: 18px; height: 18px; border-width: 2px; }
.job-dead {
  width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--surface-3); color: var(--danger);
  font-size: 18px; line-height: 1;
}
@keyframes spin { to { transform: rotate(360deg); } }

.job-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }

.running-list { display: grid; gap: 8px; margin-bottom: 28px; }
.running-row {
  display: flex; align-items: center; gap: 12px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px 14px;
  font-size: 14px;
}
.running-mode { font-weight: 600; }
.running-state { margin-inline-start: auto; color: var(--text-muted); }

.card-body-tight { padding: 8px 10px 12px; }
.card-expiry { color: var(--text-faint); font-size: 11px; }
.grid-gallery .card-media { aspect-ratio: 3 / 4; }

/* ── Daily wheel ─────────────────────────────────────────────────────────── */

.wheel-wrap { margin-top: 24px; }
.wheel-stage {
  position: relative;
  display: grid; place-items: center;
  padding-top: 16px;   /* room for the pointer above the rim */
}
.wheel { width: 100%; max-width: 320px; height: auto; display: block; }

/* Rotating the <svg> itself: an HTML-level element has an unambiguous centre,
 * so 50% 50% means the middle of the wheel on every engine. */
.wheel-rotor {
  transform-origin: 50% 50%;
  transition: transform 4.2s cubic-bezier(.16, .84, .27, 1);
  will-change: transform;
}

.wheel-seg { stroke: var(--bg); stroke-width: 1.5; }
.wheel-seg-0 { fill: var(--surface-3); }
.wheel-seg-1 { fill: color-mix(in srgb, var(--accent) 40%, var(--surface-2)); }
.wheel-rim { fill: none; stroke: var(--line-strong); stroke-width: 3; }

.wheel-label {
  fill: var(--text);
  font-size: 17px; font-weight: 700;
  font-family: var(--font);
  /* Labels ride inside the rotating group, so they must not intercept taps. */
  pointer-events: none;
}
.wheel-hub { fill: var(--bg); stroke: var(--line-strong); stroke-width: 2; }

/* Sits above the wheel and does not turn with it. */
.wheel-pointer {
  position: absolute; top: 0; left: 50%;
  transform: translateX(-50%);
  width: 0; height: 0;
  border-left: 11px solid transparent;
  border-right: 11px solid transparent;
  border-top: 20px solid var(--accent-2);
  filter: drop-shadow(0 2px 4px rgb(0 0 0 / .5));
}

.wheel-go { margin-top: 22px; }
.wheel-status {
  margin-top: 12px; text-align: center;
  color: var(--text-muted); font-size: 14px; min-height: 1.4em;
}
.wheel-status.is-win { color: var(--ok); font-weight: 650; font-size: 16px; }
.wheel-status.is-error { color: var(--danger); }
.wheel-countdown {
  text-align: center; color: var(--text-faint); font-size: 13px;
  min-height: 1.3em; margin-top: 4px;
}

@media (prefers-reduced-motion: reduce) {
  /* The result still has to arrive; it just stops being a four-second ride. */
  .wheel-rotor { transition-duration: .2s; }
}

/* ── Buying credits ──────────────────────────────────────────────────────── */

.buy-list { display: grid; gap: 10px; margin-top: 24px; }
.buy-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
}
.buy-row-featured {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
}
.buy-amount { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.buy-credits { font-weight: 700; font-size: 17px; }
.buy-save { color: var(--ok); font-size: 12px; font-weight: 650; }
.buy-flag {
  font-size: 10px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
  padding: 2px 8px; border-radius: var(--radius-pill);
}
.buy-note { margin-top: 18px; }

.notice-flat { text-align: start; justify-items: start; margin-top: 20px; }
.notice-title { font-weight: 650; }

.steps {
  list-style: decimal; padding-inline-start: 20px;
  margin: 22px 0; display: grid; gap: 10px;
  color: var(--text-muted); font-size: 15px;
}
.steps strong { color: var(--text); }

.pay-state {
  display: flex; align-items: center; gap: 10px; justify-content: center;
  margin-top: 20px;
  color: var(--text-muted); font-size: 14px;
}
.pay-state.is-done { color: var(--ok); font-weight: 650; }
.pay-done { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-top: 18px; }
.pay-stuck { margin-top: 22px; text-align: center; }

/* ── Auth ────────────────────────────────────────────────────────────────── */

.wrap-form { max-width: 420px; }

.providers { display: grid; gap: 10px; margin-top: 24px; }

.btn-provider { justify-content: center; font-weight: 600; }

/* Each mark in its own brand colours. Fixed box so the three buttons line up
 * with each other: the Google G is drawn on a 48-unit grid and the other two on
 * 24, and without this they would each render at a different visual weight.
 * flex-shrink guards the icon when a translated label runs long. */
.btn-provider svg {
  width: 20px; height: 20px; flex: 0 0 20px;
  display: block;
}
/* Telegram and Discord ship as single-colour marks, so they take the button's
 * own text colour. Google's is four-colour and sets its own fills. */
.btn-telegram svg, .btn-discord svg { fill: currentColor; }

.btn-google   { background: #ffffff; color: #1f1f1f; }
.btn-google:hover  { background: #ececec; color: #1f1f1f; }
.btn-discord  { background: #5865f2; color: #ffffff; }
.btn-discord:hover { background: #4954e8; color: #ffffff; }
/* Telegram's own brand blue. This is our button now, not the Login Widget's
   iframe — see oauth.telegram_auth_url. */
.btn-telegram { background: #2aabee; color: #ffffff; }
.btn-telegram:hover { background: #1e97d6; color: #ffffff; }

/* Rule with the label sitting in the gap. */
.divider {
  display: flex; align-items: center; gap: 12px;
  margin: 22px 0 4px;
  color: var(--text-faint); font-size: 13px;
}
.divider::before, .divider::after {
  content: ""; flex: 1; height: 1px; background: var(--line);
}

.check {
  display: flex; gap: 10px; align-items: flex-start;
  font-size: 13px; color: var(--text-muted);
  cursor: pointer;
}
.check input {
  /* Big enough to hit with a thumb, and it must not shrink next to a long
   * label — a checkbox squeezed to 4px is a checkbox nobody ticks. */
  flex: none; width: 20px; height: 20px; margin-top: 1px;
  accent-color: var(--accent);
}
.check a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }

.captcha { display: grid; gap: 10px; }
.captcha-img {
  width: 240px; max-width: 100%; height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--line-strong);
  background: var(--surface-2);
}
.captcha-input {
  text-transform: uppercase;
  letter-spacing: .28em;
  font-size: 18px;
}

.code-box {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap;
  margin-top: 22px;
  background: var(--surface);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: 16px;
}
.code-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 20px; letter-spacing: .08em;
  /* Long enough to wrap on a narrow phone — never let it push the page wide. */
  word-break: break-all;
}
.code-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: .12em; text-transform: uppercase;
}

.alert-warn {
  background: color-mix(in srgb, #e6a23c 14%, var(--surface));
  border: 1px solid color-mix(in srgb, #e6a23c 45%, transparent);
}

.form-foot {
  margin-top: 18px; text-align: center;
  font-size: 14px; color: var(--text-muted);
}
.form-foot a { color: var(--accent); }

/* ── Account ─────────────────────────────────────────────────────────────── */

.balance-chip {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--surface-2);
  border: 1px solid color-mix(in srgb, var(--accent) 55%, var(--line-strong));
  border-radius: var(--radius-pill);
  padding: 6px 13px;
  font-weight: 650; font-size: 14px;
  /* Lit from inside.
   *
   * Three layers, and each does something the others cannot: an inset ring
   * that hugs the border so the rim looks like the light source, a wider inset
   * wash so the glow falls off inwards instead of stopping at a hard edge, and
   * one outer bloom so the chip sits in its own light rather than on top of
   * the header. Colour comes from --accent, so retuning the brand moves it. */
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent),
    inset 0 0 12px color-mix(in srgb, var(--accent) 26%, transparent),
    0 0 14px color-mix(in srgb, var(--accent) 22%, transparent);
  transition: box-shadow .22s ease, border-color .22s ease;
}
.balance-chip:hover {
  border-color: var(--accent);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent),
    inset 0 0 16px color-mix(in srgb, var(--accent) 34%, transparent),
    0 0 20px color-mix(in srgb, var(--accent) 32%, transparent);
}
/* The credit unit, wherever an amount appears. Filled with the bright cyan
   rather than the gradient: it is 14px of solid shape on a near-black chip,
   and a two-stop gradient at that size just reads as muddy. */
/* inline-flex, not block. As a block it forced the number onto its own line
   every time it appeared inside a sentence or a chip — the checkout tag read
   "★" above "3600", and the cost line split in two. */
.i-credit {
  width: 14px; height: 14px; flex: 0 0 14px;
  fill: var(--accent-bright);
  display: inline-block; vertical-align: -2px;
}
.badge-price { display: inline-flex; align-items: center; gap: 4px; }
.badge-price .i-credit { width: 11px; height: 11px; flex-basis: 11px; }
.btn .i-credit { width: 15px; height: 15px; flex-basis: 15px; fill: currentColor; }
.i-settings, .i-close, .i-download, .i-lock {
  width: 18px; height: 18px; flex: 0 0 18px; display: block; fill: none;
}
.i-lock { fill: currentColor; }
.i-lock rect { fill: currentColor; }

.balance-card {
  margin: 20px 0 32px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 22px;
  display: grid; gap: 6px; justify-items: start;
}
.balance-label {
  font-size: 12px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-faint);
}
.balance-value { font-size: 32px; font-weight: 700; letter-spacing: -0.03em; }
.balance-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }

.section-title-sm { font-size: 18px; margin-bottom: 12px; }

.ledger { display: grid; gap: 1px; background: var(--line); border: 1px solid var(--line);
          border-radius: var(--radius); overflow: hidden; }
.ledger-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2px 12px;
  background: var(--surface);
  padding: 12px 14px;
  font-size: 14px;
}
.ledger-reason { font-weight: 600; }
.ledger-delta { font-weight: 700; text-align: end; font-variant-numeric: tabular-nums; }
.ledger-delta.is-plus  { color: var(--ok); }
.ledger-delta.is-minus { color: var(--text-muted); }
.ledger-time {
  grid-column: 1 / -1;
  color: var(--text-faint); font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.logout-form { margin-top: 32px; }

.hide-narrow { display: none; }

/* ── Larger screens ──────────────────────────────────────────────────────── */

@media (min-width: 480px) {
  .hide-narrow { display: inline-flex; }
}

@media (min-width: 600px) {
  .hero { padding-block: 72px 56px; }
  .hero-title { font-size: 44px; }
  .hero-sub { font-size: 18px; }
  .hero-actions { flex-direction: row; justify-content: center; }
  .section { padding-block: 56px; }
  .section-title { font-size: 28px; }
  .packs { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
  .page-title { font-size: 32px; }
}

@media (min-width: 900px) {
  .header-nav { display: flex; }
  .packs { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .detail { grid-template-columns: 5fr 6fr; gap: 32px; align-items: start; }
  .hero-title { font-size: 54px; }
}

@media (min-width: 1200px) {
}

/* ── Reduced motion ──────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}

/* Two payment methods per pack: the row keeps one label and stacks its buttons
 * rather than becoming two rows that repeat the same amount. */
.buy-methods { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.buy-methods form { display: contents; }
/* "5 credits" must not wrap to two lines when the row carries two buttons. */
.buy-credits { white-space: nowrap; }


/* ── Modals ──────────────────────────────────────────────────────────────────
 *
 * A native <dialog>. It brings focus trapping, Esc, inertness of the page
 * behind it and the top layer — all of which a hand-rolled overlay gets wrong
 * on the first mobile browser that disagrees. Everything opened this way also
 * exists as a real page, so a blocked script costs a navigation, not a feature.
 */
.modal {
  border: 0;
  padding: 0;
  background: transparent;
  max-width: min(560px, calc(100vw - 24px));
  width: 100%;
  max-height: calc(100dvh - 32px);
  color: var(--text);
  overflow: visible;
}
.modal::backdrop {
  background: rgba(4, 6, 12, .74);
  backdrop-filter: blur(6px);
}
.modal-card {
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 70px rgba(0, 0, 0, .6);
  /* The card scrolls, not the dialog: a scrolling dialog on iOS drags the page
     behind it as soon as the content is shorter than the viewport. */
  max-height: calc(100dvh - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 20px;
}
.modal-head {
  display: flex; align-items: flex-start; gap: 12px;
  margin-bottom: 14px;
}
.modal-title { font-size: 19px; font-weight: 700; line-height: 1.25; }
.modal-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
.modal-tag {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; font-weight: 600;
  padding: 3px 9px; border-radius: var(--radius-pill);
  background: var(--surface-2); color: var(--text-muted);
  white-space: nowrap;
}
.modal-close {
  margin-inline-start: auto; flex: 0 0 auto;
  background: transparent; border: 0; color: var(--text-muted);
  cursor: pointer; padding: 4px; border-radius: 13px;
  display: flex;
}
.modal-close:hover { color: var(--text); background: var(--surface-2); }
.modal-wide { max-width: min(760px, calc(100vw - 24px)); }

/* Opening animation, skipped for anyone who asked for less motion. */
@media (prefers-reduced-motion: no-preference) {
  .modal[open] { animation: modal-in .16s ease-out; }
  .modal[open]::backdrop { animation: fade-in .16s ease-out; }
}
@keyframes modal-in {
  from { opacity: 0; transform: translateY(8px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes fade-in { from { opacity: 0 } to { opacity: 1 } }

/* While a dialog is open the page behind must not scroll under it. */
body.has-modal { overflow: hidden; }

/* A spinner while a fragment is on its way. */
.modal-loading { display: grid; place-items: center; min-height: 180px; }

/* ── Option ladder (Customize Scene) ─────────────────────────────────────────
 *
 * Every rung is always visible, including the ones this account cannot use:
 * a ladder with its top missing does not sell the next tier. Locked rungs
 * carry the plan that would unlock them; unbuildable ones say "Soon" and are
 * refused by the server too, not merely dimmed here.
 */
.opt-group { margin-bottom: 16px; }
.opt-label {
  font-size: 12px; font-weight: 650; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .06em;
  margin-bottom: 7px;
}
.opt-row {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 4px; padding: 4px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
}
.opt {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  min-height: 38px; padding: 6px 4px;
  border: 1px solid transparent; border-radius: var(--radius-pill);
  background: transparent; color: var(--text-muted);
  font: inherit; font-size: 13px; font-weight: 600;
  cursor: pointer; text-align: center;
}
.opt:hover:not(:disabled) { color: var(--text); }
.opt[aria-checked="true"] {
  background: var(--bg);
  border-color: var(--accent);
  color: var(--text);
}
.opt:disabled { cursor: not-allowed; opacity: .5; }
.opt-tag {
  position: absolute; top: -8px; inset-inline-end: 2px;
  font-size: 9px; font-weight: 800; letter-spacing: .04em;
  padding: 2px 6px; border-radius: var(--radius-pill);
  background: var(--accent); color: var(--accent-ink);
  pointer-events: none;
}
.opt-tag-soon { background: var(--surface-3, #2a2a33); color: var(--text-muted); }

/* Cost and time, recomputed as the knobs move. */
.opt-summary {
  display: flex; justify-content: center; flex-wrap: wrap; gap: 6px 18px;
  font-size: 13px; color: var(--text-muted);
  margin: 4px 0 14px;
}
/* Each figure is its own inline flex row. The credit star is display:block —
   right inside a chip or a button, wrong inside a sentence, where it dropped
   the value onto its own line under the label. */
.opt-summary > span {
  display: inline-flex; align-items: center; gap: 5px;
  white-space: nowrap;
}
.opt-summary b { color: var(--text); font-weight: 650; }

/* The generate row: a settings button, then the priced call to action. */
.gen-row { display: flex; align-items: center; gap: 10px; }
.gen-row .btn { flex: 1; }
.icon-btn {
  display: flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; flex: 0 0 46px;
  border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--line-strong);
  color: var(--text-muted); cursor: pointer;
}
.icon-btn:hover { color: var(--text); border-color: var(--accent); }

/* ── Result lightbox ─────────────────────────────────────────────────────── */
.lightbox { max-width: min(920px, calc(100vw - 24px)); }
.lightbox .modal-card { padding: 12px; }
.lightbox-media {
  display: block; width: 100%; height: auto;
  max-height: calc(100dvh - 190px);
  object-fit: contain;
  background: #000; border-radius: var(--radius);
}
.lightbox-actions {
  display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px;
}
.lightbox-actions .btn { flex: 1 1 auto; }

/* ── Subscription page ───────────────────────────────────────────────────── */

.sub-current {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: 18px; margin: 4px 0 26px;
}
.sub-current-head { display: flex; align-items: baseline; gap: 10px; }
.sub-current-plan { font-size: 17px; font-weight: 700; }
.sub-current-until { color: var(--text-muted); font-size: 13px; }
.sub-current-credits {
  display: flex; align-items: center; gap: 6px;
  color: var(--text-muted); font-size: 15px; margin-top: 6px;
}
.sub-current-credits b { color: var(--text); font-size: 17px; }

.sub-details { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 12px; }
.sub-details summary {
  cursor: pointer; color: var(--text-muted); font-size: 14px; list-style: none;
}
.sub-details summary::-webkit-details-marker { display: none; }
/* Wide tables scroll inside their own box; the page itself must never scroll
   sideways on a phone. */
.table-scroll { overflow-x: auto; margin-top: 10px; }
.sub-table { width: 100%; border-collapse: collapse; font-size: 13px; white-space: nowrap; }
.sub-table th {
  text-align: start; color: var(--text-faint); font-weight: 600;
  padding: 6px 12px 6px 0; border-bottom: 1px solid var(--line);
}
.sub-table td { padding: 8px 12px 8px 0; border-bottom: 1px solid var(--line); }
.sub-table .is-spent { color: var(--text-faint); }

/* The period switch. A checkbox and sibling selectors, so it works with the
   script blocked — no JavaScript decides what a price says. */
.sub-toggle-row { display: flex; align-items: center; justify-content: center;
                  gap: 10px; margin-bottom: 18px; }
.sub-toggle {
  display: inline-flex; align-items: center; gap: 10px;
  cursor: pointer; font-size: 14px; color: var(--text-muted);
}
.sub-toggle-track {
  width: 42px; height: 24px; border-radius: var(--radius-pill);
  background: var(--surface-3); border: 1px solid var(--line-strong);
  position: relative; transition: background-color .15s ease;
  flex: none;
}
.sub-toggle-knob {
  position: absolute; top: 2px; inset-inline-start: 2px;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--text-muted); transition: transform .15s ease, background-color .15s ease;
}
.sub-toggle-input:checked ~ .sub-toggle-row .sub-toggle-track { background: var(--accent); border-color: var(--accent); }
.sub-toggle-input:checked ~ .sub-toggle-row .sub-toggle-knob { transform: translateX(18px); background: #fff; }
.sub-toggle-input:focus-visible ~ .sub-toggle-row .sub-toggle-track { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Annual is the default, so the monthly price is the one that hides. */
.sub-price-monthly { display: none; }
/* `~ *` and not `~ .sub-grid`: the grid gained a wrapper for the decorative
   band behind it, and a sibling combinator that names the grid stops matching
   the moment anything is nested between them. The prices are the only thing
   in the subtree with these classes, so the wider reach costs nothing. */
.sub-toggle-input:not(:checked) ~ * .sub-price-annual { display: none; }
.sub-toggle-input:not(:checked) ~ * .sub-price-monthly { display: inline; }

.sub-grid { display: grid; gap: 14px; }
.sub-card {
  position: relative;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: 22px 20px;
}
/* Three tiers, three genuinely different cards: grey, blue, black.
 *
 * The first attempt mixed all three from --accent, which kept them a family
 * but made Basic and Premium two shades of the same dark blue — read side by
 * side they looked like one card lit differently. These are separated by hue
 * as well as lightness, so the row reads as three things at a glance.
 *
 * Premium is the interesting one. It is black on a near-black page, and no
 * fill can distinguish it — every candidate landed at 1.03–1.14 against the
 * background, far under the ~1.2 the eye needs. So its edge does the work: a
 * defined border and a lift, which is what "black card" means anywhere it is
 * done well. */

/* Basic — slate, the way the category leader does it: lighter than the page,
   with blue in it rather than a neutral grey. The neutral version read as
   "unfinished card" next to two coloured ones; a slate reads as the entry
   tier of the same family.
 *
 * #4f5a6d is the lightest of the three candidates that still clears 4.5:1 for
 * the muted text sitting on it — the two lighter slates looked closer to the
 * reference but pushed the tagline to 3.98 and 4.33. */
.sub-card-basic {
  background: linear-gradient(160deg, #4f5a6d, #3c4553);
  border-color: #616d80;
}
/* Its own muted colour: --text-muted is tuned for the dark page and drops to
   3.2:1 on a slate this light, well under what body text needs. */
.sub-card-basic .sub-tagline,
.sub-card-basic .sub-features { color: #cfd6e0; }
.sub-card-basic .i-check { color: #eaeef4; }
.sub-card-basic .i-check-ring { fill: rgb(255 255 255 / .16); }
.sub-card-basic .i-tier { fill: #eaeef4; }
.sub-card-basic .sub-price-was { color: #b3bcc9; }

/* Pro — the accent, unchanged. */
.sub-card-pro {
  background: linear-gradient(160deg,
    color-mix(in srgb, var(--accent) 34%, var(--surface)),
    color-mix(in srgb, var(--accent-2) 12%, var(--surface)));
  border-color: var(--accent);
}

/* Premium — black, defined by its edge rather than its fill. */
.sub-card-premium {
  background: linear-gradient(160deg, #131319, #08080b);
  border-color: #39394a;
  box-shadow: 0 0 0 1px rgb(255 255 255 / .04), 0 18px 40px rgb(0 0 0 / .55);
}
.sub-card-premium .i-tier { fill: #d8dde6; }
.sub-card-premium .i-check { color: #d8dde6; }
.sub-card-premium .i-check-ring { fill: rgb(216 221 230 / .14); }

/* The ring around whichever tier is popular, kept independent of which one
   that happens to be. */
.sub-card-popular { border-color: var(--accent); }
.sub-card-current { border-color: var(--line-strong); }
.sub-flag {
  position: absolute; top: -11px; left: 50%; transform: translateX(-50%);
  background: var(--text); color: var(--bg);
  font-size: 11px; font-weight: 800; letter-spacing: .04em;
  padding: 3px 12px; border-radius: var(--radius-pill);
  white-space: nowrap;
}
.sub-tagline { color: var(--text-faint); font-size: 12px; }
.sub-name { font-size: 24px; margin: 2px 0 12px; }
.sub-price { display: flex; align-items: baseline; gap: 8px; margin-bottom: 16px; }
.sub-price b { font-size: 32px; font-weight: 700; letter-spacing: -0.03em; }
.sub-price-was { color: var(--text-faint); font-size: 17px; }
.sub-per { color: var(--text-muted); font-size: 14px; }
.sub-features { margin-top: 16px; display: grid; gap: 9px; font-size: 14px; }
.sub-features li { display: flex; gap: 9px; color: var(--text-muted); }

.sub-features b { color: var(--text); }

.sub-onetime {
  text-align: center; color: var(--text-muted);
  font-size: 13px; margin: 22px 0 8px;
}
.sub-faq-title { margin-top: 34px; margin-bottom: 14px; }

.faq { display: grid; gap: 8px; }
.faq-item {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
}
.faq-item summary {
  cursor: pointer; font-weight: 600; list-style: none;
  display: flex; justify-content: space-between; gap: 12px;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-body { color: var(--text-muted); font-size: 14px; margin-top: 9px; }

/* The checkout sheet's accordion is defined once, at the end of this file. */

@media (min-width: 760px) {
  .sub-grid { grid-template-columns: repeat(3, 1fr); align-items: start; }
  .sub-card-popular { transform: scale(1.02); }
}

/* ── Generate sheet ──────────────────────────────────────────────────────── */

/* Before and after, side by side with an arrow between — the shape this
   audience already reads as "put a photo in, get this out". */
.pair-modal {
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 8px; margin-bottom: 14px;
}
.pair-modal .pair-frame {
  position: relative; margin: 0;
  border-radius: var(--radius); overflow: hidden;
  background: var(--surface-2); aspect-ratio: 3 / 4;
}
.pair-modal .pair-media { width: 100%; height: 100%; object-fit: cover; }
.pair-modal .pair-tag {
  position: absolute; inset-inline-start: 8px; bottom: 8px;
  font-size: 10px; font-weight: 700;
  padding: 3px 8px; border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: blur(6px);
}
/* Scoped, and every inherited property reset explicitly. The older .pair-arrow
   is a filled circle and the older .drop-cta covers the whole frame; without
   these resets the chevron renders as a circle with two stray borders and the
   upload label stretches into a full-height ellipse. */
.pair-modal .pair-arrow {
  width: 18px; height: 18px; flex: none;
  background: none; border-radius: 0;
  border-top: 2px solid var(--accent-bright);
  border-right: 2px solid var(--accent-bright);
  border-left: 0; border-bottom: 0;
  transform: rotate(45deg);
  opacity: .85;
  justify-self: center;
}
.pair-drop { cursor: pointer; display: block; }
.pair-modal .pair-drop .drop-cta {
  position: absolute; inset: auto 8px 8px 8px;
  display: flex; justify-content: center;
  padding: 7px 10px; border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: blur(6px);
  font-size: 12px; font-weight: 650;
}
.pair-drop.has-file { outline: 2px solid var(--accent); outline-offset: -2px; }

.gen-form .check { margin: 12px 0 4px; }
.modal-lede { margin: 10px 0 14px; }

/* The ladder, revealed in place by the gear. */
.scene {
  margin-top: 14px; padding-top: 14px;
  border-top: 1px solid var(--line);
}

/* On the standalone page the same card must not look like a floating dialog. */
.mode-page .modal-card {
  max-height: none; overflow: visible;
  padding: 0; background: transparent; border: 0; box-shadow: none;
}

/* Plan teaser on the landing page — three tiles that lead to /subscription. */
.plan-teaser { display: grid; gap: 10px; margin-bottom: 26px; }
.plan-tile {
  position: relative; display: block;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 16px;
}
.plan-tile:hover { border-color: var(--line-strong); }
.plan-tile-popular {
  background: linear-gradient(160deg,
    color-mix(in srgb, var(--accent) 26%, var(--surface-2)), var(--surface-2));
  border-color: var(--accent);
}
.plan-tile-name { font-weight: 700; font-size: 15px; }
.plan-tile-price { display: flex; align-items: baseline; gap: 6px; margin-top: 4px; }
.plan-tile-price b { font-size: 24px; letter-spacing: -0.02em; }
.plan-tile-credits { color: var(--text-muted); font-size: 13px; margin-top: 2px; }
@media (min-width: 700px) { .plan-teaser { grid-template-columns: repeat(3, 1fr); } }

/* On the standalone page the sheet is the page: no dismiss control, and the
   heading belongs to the document rather than to a dialog. */
.mode-page .modal-close { display: none; }
.mode-page .modal-head { margin-bottom: 18px; }
.mode-page .modal-title { font-size: 24px; }

/* Preview panel: the still is the floor, the clip is revealed over it. Same
   arrangement as the catalogue cards, and for the same reason — frame 0 of an
   image-to-video clip is the input photo. */
/* Visible from the start, unlike the catalogue cards. A card can afford to
   hide a clip until it plays because there are 25 others to look at; this is
   the one panel the visitor opened, and a refused autoplay would leave it
   showing the poster with no way to tell that anything is wrong. Seeking to
   45% is what makes that safe — a paused clip here shows the result, never the
   input photo. */
.pair-clip {
  position: relative;
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  /* No motion wanted. The clip stays — it is the only thing showing the result
     now that the poster stage is gone — but it is not played; app.js checks the
     same preference and leaves it parked on the frame it seeked to. */
}

/* ── Blur-up placeholders ────────────────────────────────────────────────────
 *
 * A 20px JPEG carried inline in the HTML, scaled up and blurred. It is the
 * bottom of three layers: this, then the poster still, then the clip. The
 * point is that a card is never an empty box — on a phone the poster stills
 * alone are around a megabyte, and until they land the grid would be 26 grey
 * rectangles.
 *
 * `filter: blur()` on a 20px source scaled to card size is what turns it from
 * visible pixels into something that reads as the picture. The scale(1.06)
 * hides the soft edge the blur leaves at the border.
 */
.card-lqip, .pair-lqip {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  filter: blur(12px) saturate(1.1);
  transform: scale(1.06);
  z-index: 0;
}

/* The 1.06 scale on the placeholder exists to hide the soft edge blur leaves,
   which means it deliberately overflows — so the frame has to clip it, or the
   blur bleeds past the card's corners. */
.card-media, .pair-frame { position: relative; overflow: hidden; }
/* The overlays have to sit above the image stack. Giving the still a z-index
   lifted it over every positioned sibling that had none — which is how the
   price badge and the "your photo" thumbnail vanished. Numbering them all
   explicitly is the only way this stays true when a layer is added later. */
/* z-index only — these are already absolutely positioned, and restating
   `position` here moved the thumbnail out of its corner. */
.card-badges, .card-source, .pair-tag, .drop-cta { z-index: 3; }

/* ── The strip under the header ──────────────────────────────────────────── */

.banner {
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
  text-align: center;
  font-size: 12px; font-weight: 700; letter-spacing: .04em;
  padding: 6px 16px;
  /* One line, always. A wrapped banner pushes the grid down, which is the one
     thing this layout exists to avoid. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ── Compact intro ───────────────────────────────────────────────────────── */

.lede-strip { padding: 18px 0 6px; }
.lede-title { font-size: 22px; letter-spacing: -0.02em; }
.lede-sub { color: var(--text-muted); font-size: 14px; margin-top: 4px; }

/* The catalogue starts immediately under it. */
.section-catalog { padding-top: 12px; }

@media (min-width: 700px) {
  .banner { font-size: 13px; padding: 7px 16px; }
  .lede-strip { padding: 26px 0 10px; }
  .lede-title { font-size: 30px; }
  .lede-sub { font-size: 15px; }
}

/* ── Bigger cards ────────────────────────────────────────────────────────── */

/* Edge to edge on a phone: the 16px gutter either side costs a tenth of the
   card width at 375px, and these cards are the product. */
@media (max-width: 559px) {
  .section-catalog .wrap { padding-inline: 8px; }
  .grid { gap: 8px; }
}

/* ── Full-bleed cards ────────────────────────────────────────────────────────
 *
 * The tile IS the video. Everything else floats on top of it.
 *
 * The old card put the title and description in a block underneath, which on a
 * phone cost roughly a third of every tile to repeat what the picture already
 * said — and pushed the second row of the grid below the fold. This is the
 * arrangement the category leader uses, and it is right for a catalogue whose
 * whole pitch is "look at the result".
 */
.card { border-radius: var(--radius); overflow: hidden; }
.card-link { display: block; height: 100%; }

/* One class per shape the catalogue can hold. A card reserves its box before
   any media arrives, so nothing shifts as clips load, and a grid of mixed
   portraits, squares and landscapes lays out without a line of JavaScript. */
.ar-9x16 { aspect-ratio: 9 / 16; }
.ar-3x4  { aspect-ratio: 3 / 4;  }
.ar-1x1  { aspect-ratio: 1 / 1;  }
.ar-4x3  { aspect-ratio: 4 / 3;  }
.ar-16x9 { aspect-ratio: 16 / 9; }
.card-media > .card-img,
.card-media > .card-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}




/* Badges top-left, the input photo top-right.
 *
 * The thumbnail is deliberately small. At 34% of a tile it swallowed the top
 * corner and, on a phone where the tile is only ~170px wide, landed straight
 * across the model's face — the one part of the frame the card exists to show.
 * The reference site keeps this element at roughly a fifth of the tile, and
 * that is the right instinct: it is a hint about the input, not a second
 * picture competing with the result. Capped in absolute terms too, so it does
 * not grow into a wide desktop tile. */
.card-badges { top: 8px; inset-inline-start: 8px; inset-inline-end: auto; }

/* No caption. At this size it can only ever be truncated, and the thumbnail
   needs no label on a page whose whole proposition is "your photo becomes
   this". The reference site does not label it either. */
.card-source-tag { display: none; }
@media (max-width: 559px) {  }

@media (min-width: 700px) {
  
  
}


/* ── The image stack, stated once ────────────────────────────────────────────
 *
 * Four layers, and the order is the whole feature. It was previously spread
 * across three places in this file and they disagreed: the poster carried
 * `z-index: 1` while the clip carried none, so the poster painted OVER the
 * video and the tile showed a still no matter how well playback was going.
 * That is the "videos do not play" bug — the video was playing the entire
 * time, underneath.
 *
 * Explicit numbers on every layer, at the end of the file, so nothing above
 * can quietly win again.
 */
.card-lqip, .pair-lqip   { z-index: 0; }  /* blurred 20px thumbnail, inlined   */
.card-poster             { z-index: 1; }  /* sharp still, ~15 KB, lazy         */
.card-clip, .pair-clip   { z-index: 2; }  /* the video, over both              */
.card-badges, .card-source, .card-overlay,
.pair-tag, .drop-cta     { z-index: 3; }   /* chrome, over everything          */

/* Always visible — no class to wait for, nothing to get stuck on.
 *
 * The clip sits ABOVE the poster now, and a <video> with no decoded frames
 * paints nothing, so the poster below simply shows through until the first
 * frame arrives and then is covered. The result is the same fade-in the old
 * arrangement was reaching for, produced by the layering itself rather than by
 * a JavaScript class that three separate bugs had already managed to leave
 * unset — each one presenting as "the videos do not play" while the video was
 * playing invisibly underneath a poster. */
.card-clip { opacity: 1; }

/* The catalogue.
 *
 * A row-major grid, not multi-column masonry.
 *
 * Masonry was here first, so that a tile of any shape could keep its own
 * height. It cost the reading order, and that turned out to be the expensive
 * half of the trade: `column-count` fills top-to-bottom, so the first four
 * tiles a visitor sees across the top row are items 1, 7, 13 and 19. The
 * catalogue looked shuffled — videos and photos alternating for no reason —
 * even though modes.py lists them in a deliberate order.
 *
 * A grid flows left to right, so what the server sends is what is read: the
 * free trial, then the plain undress photo, then the videos, then the rest.
 *
 * Each tile still carries its own shape through the .ar-* classes on
 * .card-media, so mixed uploads are not distorted. What changes is the packing:
 * a grid row is as tall as its tallest tile, where masonry would have closed
 * the gap. With all 26 previews on one shape today that costs nothing, and
 * ordered-with-gaps beats tightly-packed-and-shuffled.
 */
.grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  align-items: start;
}

@media (min-width: 560px)  { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } }
/* Fewer columns from here up, by owner decision: on a desktop the tiles were
   too small to read the result at a glance, which is the one thing a tile is
   for. The phone breakpoints above are unchanged — they were already right. */
@media (min-width: 1000px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1400px) { .grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* ── Motion ──────────────────────────────────────────────────────────────────
 *
 * Every animation below is decoration and every one of them is switched off
 * under prefers-reduced-motion at the end of this block. Nothing here is the
 * only way to perceive a state change.
 *
 * Only transform and opacity are animated: they run on the compositor, so a
 * grid of 26 playing videos does not drop frames while a card lifts.
 */

/* Tiles rise slightly and the clip inside pushes in, so the motion reads as
   one object rather than a box and its contents moving separately. */
.card {
  transition: transform .22s cubic-bezier(.2, .7, .3, 1),
              box-shadow .22s ease;
  will-change: transform;
}
.card-media > .card-img,
.card-media > .card-video { transition: transform .4s cubic-bezier(.2, .7, .3, 1); }
@media (hover: hover) {
  .card:hover { transform: translateY(-3px); box-shadow: var(--shadow-lift); }
  .card:hover .card-img, .card:hover .card-video { transform: scale(1.04); }
  .card:hover 
}
.card:active { transform: translateY(0) scale(.985); }

/* Filter chips: the selected one grows into place instead of blinking. */
.chip { transition: background-color .18s ease, color .18s ease, transform .18s ease; }
.chip:active { transform: scale(.94); }

/* Buttons: a short, springy press. */
.btn { transition: transform .12s cubic-bezier(.3, 1.4, .5, 1),
                   filter .15s ease, background-color .15s ease; }
.btn:active { transform: scale(.97); }

/* Option pills in Customise scene. */
.opt { transition: background-color .16s ease, border-color .16s ease, color .16s ease; }
.opt[aria-checked="true"] { animation: opt-pick .22s cubic-bezier(.3, 1.5, .5, 1); }
@keyframes opt-pick { from { transform: scale(.93); } to { transform: none; } }

/* The catalogue arriving. Staggered by column so the grid assembles rather
   than appearing all at once — six steps, then it repeats, which is enough to
   read as a wave without tracking every index. */
@media (prefers-reduced-motion: no-preference) {
  .grid > .card { animation: card-in .38s cubic-bezier(.2, .7, .3, 1) both; }
  .grid > .card:nth-child(6n+2) { animation-delay: .04s; }
  .grid > .card:nth-child(6n+3) { animation-delay: .08s; }
  .grid > .card:nth-child(6n+4) { animation-delay: .12s; }
  .grid > .card:nth-child(6n+5) { animation-delay: .16s; }
  .grid > .card:nth-child(6n+6) { animation-delay: .20s; }
}
@keyframes card-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* The blurred placeholder fades out under the clip rather than being cut. */
.card-lqip, .pair-lqip { transition: opacity .35s ease .05s; }

@media (prefers-reduced-motion: reduce) {
  .card, .card-img, .card-video, .chip, .btn, .opt,
  .grid > .card, .card-lqip, .pair-lqip {
    transition: none !important;
    animation: none !important;
  }
}

/* The catalogue runs wider than the reading column. Prose wants a measure;
   a wall of video wants the screen — the reference site gives its grid the
   full width and it is right to. Text sections keep the narrower --wrap. */
.section-catalog > .wrap { max-width: 1720px; }

/* ── Subscription page ───────────────────────────────────────────────────── */

/* A soft band behind the plans. Two ellipses in an SVG rather than a CSS
   gradient because they drift independently, which a single background cannot
   do without repainting the whole element every frame. */
.sub-stage { position: relative; }
.sub-glow {
  position: absolute; inset: -60px -40px auto -40px;
  height: 340px; width: calc(100% + 80px);
  z-index: 0; pointer-events: none;
  filter: blur(38px);
  opacity: .55;
}
.sub-stage > .sub-grid { position: relative; z-index: 1; }
@media (prefers-reduced-motion: no-preference) {
  .sub-glow-a { animation: drift-a 18s ease-in-out infinite alternate; }
  .sub-glow-b { animation: drift-b 22s ease-in-out infinite alternate; }
}
@keyframes drift-a {
  from { transform: translate3d(-30px, -10px, 0) scale(1); }
  to   { transform: translate3d(40px, 18px, 0) scale(1.08); }
}
@keyframes drift-b {
  from { transform: translate3d(30px, 12px, 0) scale(1.06); }
  to   { transform: translate3d(-36px, -14px, 0) scale(1); }
}

/* Tier mark beside the plan name. */
.sub-name { display: flex; align-items: center; gap: 9px; }
.i-tier {
  width: 22px; height: 22px; flex: none;
  fill: var(--accent-bright);
}
.sub-card-popular .i-tier { fill: #fff; }
.i-tier-eye { fill: var(--surface); }

/* Drawn ticks, aligned to the first line of a wrapping feature. */
.sub-features li { align-items: flex-start; gap: 10px; }
.i-check {
  width: 17px; height: 17px; flex: none;
  margin-top: 2px;
  color: var(--accent-bright);
}
.i-check-ring { fill: color-mix(in srgb, var(--accent-bright) 16%, transparent); }
.sub-card-popular .i-check { color: #fff; }
.sub-card-popular .i-check-ring { fill: rgb(255 255 255 / .18); }

/* Cards arrive in sequence, and the popular one settles last so the eye ends
   on it. */
@media (prefers-reduced-motion: no-preference) {
  .sub-card { animation: plan-in .45s cubic-bezier(.2, .7, .3, 1) both; }
  .sub-card:nth-child(2) { animation-delay: .07s; }
  .sub-card:nth-child(3) { animation-delay: .14s; }
  .sub-flag { animation: flag-in .5s cubic-bezier(.3, 1.5, .5, 1) .3s both; }
}
@keyframes plan-in {
  from { opacity: 0; transform: translateY(14px) scale(.98); }
  to   { opacity: 1; transform: none; }
}
@keyframes flag-in {
  from { opacity: 0; transform: translate(-50%, 6px) scale(.9); }
  to   { opacity: 1; transform: translate(-50%, 0) scale(1); }
}

/* The price swapping between annual and monthly. Both are in the DOM, so this
   is a crossfade rather than a reflow. */
.sub-price b, .sub-price-was, .sub-per {
  transition: opacity .2s ease, transform .2s ease;
}
.sub-price-annual, .sub-price-monthly { animation: price-in .28s ease both; }
@keyframes price-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

.sub-card {
  transition: transform .25s cubic-bezier(.2, .7, .3, 1), border-color .25s ease;
}
@media (hover: hover) {
  .sub-card:hover { transform: translateY(-4px); border-color: var(--accent-bright); }
}
.sub-toggle-track, .sub-toggle-knob {
  transition: background-color .2s ease, transform .25s cubic-bezier(.3, 1.4, .5, 1);
}

/* FAQ rows open with a little give rather than snapping. */
.faq-item { transition: border-color .2s ease, background-color .2s ease; }
.faq-item[open] { border-color: var(--line-strong); }
@media (prefers-reduced-motion: no-preference) {
  .faq-item[open] .faq-body { animation: faq-open .24s ease both; }
}
@keyframes faq-open {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .sub-card, .sub-flag, .sub-glow-a, .sub-glow-b,
  .sub-price-annual, .sub-price-monthly, .faq-item[open] .faq-body {
    animation: none !important; transition: none !important;
  }
}

/* ── Right-to-left ───────────────────────────────────────────────────────── */
/* Arabic. The layout mirrors on its own — the rules above use logical
   properties — so only the handful of things that carry direction in their
   geometry rather than in a margin are restated here. */
[dir="rtl"] .i-download { transform: scaleX(-1); }
/* The knob is a circle — mirroring it does nothing, and a transform here would
   fight the translate that moves it. What actually has to flip is the
   direction of that travel. */
[dir="rtl"] .sub-toggle-input:checked ~ .sub-toggle-row .sub-toggle-knob {
  transform: translateX(-18px);
}
[dir="rtl"] .drop-arrow { transform: scaleX(-1); }
/* Prices, balances and durations stay left-to-right inside RTL text: an
   amount is a number, and "30 ⭐" reversed reads as a different number. */
[dir="rtl"] .badge-price, [dir="rtl"] .sub-price, [dir="rtl"] .ledger-delta,
[dir="rtl"] .balance-value, [dir="rtl"] .credit-amount {
  direction: ltr; unicode-bidi: isolate;
}

/* ── Header on a narrow screen ───────────────────────────────────────────── */
/* Three controls plus a language picker do not fit across 375px, and the
   overflow showed as "Get started" breaking onto a second line and pushing
   the header to 61px. Nothing is removed — the pieces are just sized for the
   room actually available. */
.header-actions .btn { white-space: nowrap; }

@media (max-width: 599px) {
  /* Sign-in lives on the signup page one tap away, and a visitor who has
     never signed up is the one this header is for. */
  .header-actions .btn-ghost { display: none; }
  .header-actions { gap: 6px; }
  .header-actions .btn { padding: 8px 14px; font-size: 14px; }
  /* Wide enough for the longest endonym that actually appears once the
     picker is sized for a phone, so nothing in it reads as cut off. */
  .lang-select {
    padding: 7px 10px; font-size: 13px;
    max-width: 8em; text-overflow: ellipsis;
  }
}

/* The banner is one line by design, but at 375px that line ended in an
   ellipsis mid-pitch — "new ones every …" reads as broken, not as brief. On a
   phone it wraps instead. The second line costs ~16px, which is cheaper than
   losing the sentence, and the grid below still starts above the fold. */
@media (max-width: 599px) {
  .banner {
    white-space: normal; overflow: visible; text-overflow: clip;
    font-size: 11.5px; letter-spacing: .02em; line-height: 1.35;
    padding: 5px 14px;
  }
}

/* ── Checkout sheet ──────────────────────────────────────────────────────── */

/* Which plan, which period. No amount — see the note in _pay_modal.html. */
.pay-plan-pill {
  display: inline-block;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
  font-weight: 700; font-size: 15px;
  padding: 9px 16px; border-radius: var(--radius-pill);
}

.pay-acc { border-top: 1px solid var(--line); }
.pay-acc:first-of-type { border-top: 0; }
.pay-acc > summary {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  padding: 16px 2px;
  font-weight: 700; font-size: 17px;
  cursor: pointer;
  list-style: none;               /* Firefox */
}
/* The native disclosure triangle cannot be styled to match across browsers, so
   it is removed and the chevron in the markup is used instead. */
.pay-acc > summary::-webkit-details-marker { display: none; }
.pay-acc > summary::marker { content: ""; }
/* No character marker here: the row draws an SVG chevron instead, and the
   "⌄"/"⌃" rules this design replaced were deleted rather than overridden. */
.pay-chevron {
  width: 20px; height: 20px; flex: none;
  color: var(--text-muted);
  transition: transform .2s ease;
}
.pay-acc[open] > summary .pay-chevron { transform: rotate(180deg); }
.pay-acc > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.pay-body {
  padding: 0 2px 18px;
  display: grid; gap: 12px;
  font-size: 14.5px; color: var(--text-muted);
}
.pay-steps {
  margin: 0; padding-inline-start: 22px;
  list-style: decimal;
  display: grid; gap: 8px;
  color: var(--text-muted); font-size: 14.5px; line-height: 1.5;
}
.pay-steps li::marker { color: var(--accent-bright); font-weight: 700; }

@media (prefers-reduced-motion: no-preference) {
  .pay-acc[open] > .pay-body { animation: faq-open .22s ease both; }
}

/* The checkout button. White, because that is what leaving for Telegram looks
   like everywhere else the user has seen it, and because a second accent-blue
   button next to the accent-blue pill above reads as the same control twice.
 *
 * NOT .btn-telegram — that class belongs to the sign-in-with-Telegram provider
 * button, which sets color:#fff on hover and fills its icon with currentColor.
 * Sharing it put a white label on a white button and turned the plane black. */
.btn-pay-telegram {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  background: #fff; color: #12121a;
  border: 1px solid #fff;
  font-weight: 700;
}
/* The label is restated on every state. Without it the generic .btn rules
   decide what colour text is on a white button, which is how this broke. */
.btn-pay-telegram:hover {
  background: #e8e8ef; border-color: #e8e8ef; color: #12121a;
}
.btn-pay-telegram:active,
.btn-pay-telegram:focus,
.btn-pay-telegram:visited { color: #12121a; }
.btn-pay-telegram:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 2px; }

/* The plane keeps Telegram's own light blue on every state — !important
   because .btn-provider svg and friends target `svg` by element and outrank a
   single class no matter where this sits in the file. */
.btn-pay-telegram .i-telegram { width: 21px; height: 21px; flex: none; }
.btn-pay-telegram .i-telegram path { fill: #29b6f6 !important; }
.btn-pay-telegram .i-telegram .i-telegram-fold { fill: #0f9bdc !important; }

/* ── Sheen on the primary buttons ────────────────────────────────────────── */
/* A highlight that travels across the button every few seconds, the way the
 * category leader's plan buttons do.
 *
 * A pseudo-element rather than an animated background-position: the button
 * already carries a gradient, and animating that gradient would fight it. This
 * lays a separate transparent-white band on top and slides only that, which
 * composites on the GPU and never repaints the button itself.
 *
 * Scoped to the plan buttons, not every .btn-primary on the site: a sheen on
 * the twentieth button a page shows is decoration, on the one button that
 * matters it is a cue. */
.sub-card .btn-primary {
  position: relative;
  overflow: hidden;
  isolation: isolate;      /* keeps the band inside the rounded corners */

  /* The shadow under Get Started, back by owner request and measured off the
     reference site rather than guessed: theirs is 0 4px 4px at 12% black on a
     100px pill.
   *
   * Two layers instead of their one. Theirs sits on a light grey card where
   * 12% black is plainly visible; our three cards run from grey to dark navy,
   * and on the navy one that layer disappears entirely. The tight contact
   * shadow is theirs exactly — it is what makes the button read as raised —
   * and the wider one under it is what carries that reading onto the dark
   * card.
   *
   * This replaces the diffuse accent-tinted glow the button has elsewhere. A
   * glow says "this is coloured"; a contact shadow says "this is on top of
   * something", and on a plan card the second is the one worth saying. */
  box-shadow: 0 4px 4px rgb(0 0 0 / .12),
              0 10px 20px rgb(0 0 0 / .26);
}
.sub-card .btn-primary:hover {
  box-shadow: 0 5px 6px rgb(0 0 0 / .16),
              0 14px 26px rgb(0 0 0 / .3);
}
.sub-card .btn-primary:active {
  /* Pressed: the button comes down to the card, so the gap closes. */
  box-shadow: 0 1px 2px rgb(0 0 0 / .2);
  transform: translateY(1px);
}
.sub-card .btn-primary::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(105deg,
    transparent 35%,
    rgb(255 255 255 / .38) 50%,
    transparent 65%);
  transform: translateX(-120%);
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .sub-card .btn-primary::after {
    animation: sheen 3.6s cubic-bezier(.4, 0, .3, 1) infinite;
  }
  /* Staggered, so three buttons in a row do not flash in unison — that reads
     as the page glitching rather than as the buttons being alive. */
  .sub-card:nth-child(2) .btn-primary::after { animation-delay: .5s; }
  .sub-card:nth-child(3) .btn-primary::after { animation-delay: 1s; }
}
@keyframes sheen {
  /* The travel is a fifth of the cycle; the rest is the pause between passes.
     Without the pause it is a strobe. */
  0%   { transform: translateX(-120%); }
  22%  { transform: translateX(120%); }
  100% { transform: translateX(120%); }
}
/* On hover the sheen sweeps once, immediately — the button answers the cursor
   instead of waiting out its cycle. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .sub-card .btn-primary:hover::after {
    animation: sheen 1.1s cubic-bezier(.4, 0, .3, 1);
  }
}

/* ── Telegram payment tutorial ───────────────────────────────────────────── */
/* Narrow measure: this is the one page here people read rather than scan, and
   a 1700px line of body text is unreadable however good the copy is. */
.wrap-prose { max-width: 760px; }

.tut-lede {
  font-size: 17px; line-height: 1.6; color: var(--text-muted);
  margin: 6px 0 26px;
}
.tut-h2 { font-size: 19px; font-weight: 700; margin: 0 0 8px; }
.tut-why, .tut-after {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: 18px 20px;
  margin-bottom: 22px;
  color: var(--text-muted); line-height: 1.6;
}

/* The steps carry their number from the list, not from the copy — a translator
   who reorders a sentence cannot renumber the page. */
.tut-steps {
  list-style: none; counter-reset: tut;
  margin: 0 0 22px; padding: 0;
  display: grid; gap: 16px;
}
.tut-step {
  counter-increment: tut;
  display: grid; grid-template-columns: auto 1fr; gap: 16px; align-items: start;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: 20px;
}
.tut-step::before {
  content: counter(tut);
  display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  color: var(--accent-ink); font-weight: 800; font-size: 16px;
}
.tut-step-body { color: var(--text-muted); line-height: 1.6; }
.tut-step-body p { margin: 0 0 10px; }
.tut-step-body p:last-child { margin-bottom: 0; }

.tut-tip, .tut-warn {
  display: flex; gap: 10px; align-items: flex-start;
  border-radius: var(--radius-md); padding: 12px 14px;
  font-size: 14.5px;
}
.tut-tip {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
  color: var(--text);
}
/* Amber, not the accent: this one is the mistake people actually make, and it
   has to read as different from the tip above it rather than as more of it. */
.tut-warn {
  background: rgb(224 158 42 / .12);
  border: 1px solid rgb(224 158 42 / .38);
  color: #f0d9a8;
}
.i-info, .i-warn { width: 19px; height: 19px; flex: none; margin-top: 1px; }
.i-info { color: var(--accent-bright); }
.i-warn { color: #e6a83c; }

.tut-trouble-title { margin: 30px 0 12px; }
.tut-cta { margin-top: 26px; }

/* Wherever the tutorial is linked from — checkout sheet, waiting page. */
.pay-tutorial-link {
  display: inline-block;
  font-size: 14px; color: var(--accent-bright);
  text-decoration: underline; text-underline-offset: 3px;
}

@media (max-width: 599px) {
  .tut-step { grid-template-columns: 1fr; gap: 12px; padding: 16px; }
}

/* ── Disclosure rows ─────────────────────────────────────────────────────── */
/* Every <details> on the site — balance breakdown, FAQ, tutorial, checkout —
 * uses one chevron and one animation.
 *
 * It replaces "⌄"/"⌃" (U+2304/U+2303). Those are characters, and no UI font
 * places them on the baseline the same way, so the arrow sat visibly low next
 * to its label in every row that used one. An SVG has a box we control, so it
 * aligns by construction rather than by luck. */
.sub-details > summary,
.faq-item > summary {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  list-style: none;
}
.sub-details > summary::-webkit-details-marker,
.faq-item > summary::-webkit-details-marker { display: none; }
.sub-details > summary::marker,
.faq-item > summary::marker { content: ""; }

.disclosure-chevron {
  width: 18px; height: 18px; flex: none;
  color: var(--text-muted);
  transition: transform .22s cubic-bezier(.2, .7, .3, 1);
}
.sub-details[open] > summary .disclosure-chevron,
.faq-item[open] > summary .disclosure-chevron { transform: rotate(180deg); }
.sub-details > summary .disclosure-chevron { width: 16px; height: 16px; }

/* Opening and closing, smoothly.
 *
 * ::details-content is the element the browser wraps a <details> body in, and
 * `interpolate-size` is what makes height: 0 -> auto animatable at all — until
 * it existed this needed either a magic max-height or a wrapper element and the
 * 0fr/1fr grid trick. `content-visibility` has to transition too, with
 * allow-discrete, or the content vanishes on frame one of the close and the
 * height animates against nothing.
 *
 * Behind @supports on purpose: where it is unavailable the panels simply open
 * instantly, which is exactly what they did before. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }

  .sub-details::details-content,
  .faq-item::details-content,
  .pay-acc::details-content {
    block-size: 0;
    overflow: hidden;
    opacity: 0;
    transition:
      block-size .3s cubic-bezier(.2, .7, .3, 1),
      opacity .22s ease,
      content-visibility .3s allow-discrete;
  }
  .sub-details[open]::details-content,
  .faq-item[open]::details-content,
  .pay-acc[open]::details-content {
    block-size: auto;
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .disclosure-chevron, .pay-chevron { transition: none; }
  .sub-details::details-content,
  .faq-item::details-content,
  .pay-acc::details-content { transition: none; }
}

/* The customise-scene panel, opened by the gear next to Generate.
 *
 * Not a <details> — it lives inside the generate form so the ladder writes
 * straight to the hidden inputs — so it gets the same treatment by hand.
 *
 * Behind the same @supports as the disclosure rows, and app.js checks the same
 * thing before it takes this path at all. That pairing matters: this rule
 * replaces the `hidden` attribute with a collapsed box, and a browser that
 * ignored interpolate-size would render that box collapsed with nothing able
 * to expand it — a settings panel that cannot be opened. Where the support is
 * absent, the script keeps toggling `hidden` and none of this applies.
 *
 * content-visibility, not only height: a collapsed panel with height 0 still
 * holds its radio buttons in the tab order, so keyboard focus disappears into
 * a panel nobody can see. */
@supports (interpolate-size: allow-keywords) {
  .scene-collapsible {
    block-size: 0;
    overflow: hidden;
    opacity: 0;
    content-visibility: hidden;
    transition:
      block-size .3s cubic-bezier(.2, .7, .3, 1),
      opacity .22s ease,
      content-visibility .3s allow-discrete;
  }
  .scene-collapsible.is-open {
    block-size: auto;
    opacity: 1;
    content-visibility: visible;
  }
}

/* The rows inside arrive in sequence, so the panel unfolds rather than
   appearing all at once. */
@media (prefers-reduced-motion: no-preference) {
  .scene-collapsible.is-open .opt-group {
    animation: opt-in .28s cubic-bezier(.2, .7, .3, 1) both;
  }
  .scene-collapsible.is-open .opt-group:nth-of-type(2) { animation-delay: .05s; }
  .scene-collapsible.is-open .opt-group:nth-of-type(3) { animation-delay: .1s; }
}
@keyframes opt-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .scene-collapsible { transition: none; }
  .scene-collapsible .opt-group { animation: none; }
}

/* ── Welcome offer ───────────────────────────────────────────────────────── */
.offer-strip {
  /* Flat #dc9546, not a gradient.
   *
   * It was a two-stop ramp from #c2742a, which crossed a hue nobody chose:
   * the middle of that blend is neither the warm accent nor the darker one,
   * and on a strip this shallow it read as a mistake rather than as depth.
   * The colour was picked as a colour; it is used as one. */
  background: var(--accent-warm);
  /* Dark text, not white. #dc9546 is a light colour: white on it measures
     2.49:1, well under readable, while near-black reaches 7.4. */
  color: #17130c; text-align: center;
  font-size: 13px; font-weight: 700; letter-spacing: .02em;
  padding: 7px 16px;
}
/* Warm, not the site accent. The accent means "our brand" on every other
   surface; a countdown that borrows it reads as chrome and gets ignored. */
.sub-offer-badge {
  position: absolute; top: 14px; inset-inline-end: 14px;
  background: var(--accent-warm); color: #17130c;
  font-weight: 800; font-size: 13px;
  padding: 4px 10px; border-radius: var(--radius-pill);
}

/* ── First-run tour ──────────────────────────────────────────────────────── */
/* The dimmer: four panels around a hole. Not one element with a giant
 * box-shadow — that looks the same and still intercepts every click, so the
 * highlighted control would be visible and unusable, which defeats the point.
 */
.tour { position: fixed; inset: 0; z-index: 90; pointer-events: none; }
.tour-mask {
  position: fixed; background: rgb(0 0 0 / .72);
  pointer-events: auto;                 /* everything outside the hole is blocked */
  transition: none;
}
[data-tour-mask-top]    { inset-inline: 0; top: 0; }
[data-tour-mask-bottom] { inset-inline: 0; bottom: 0; }
[data-tour-mask-left]   { inset-inline-start: 0; }
[data-tour-mask-right]  { inset-inline-end: 0; }

/* The outline around the hole. Purely decorative and never in the way. */
.tour-ring {
  position: fixed; pointer-events: none;
  border: 2px solid var(--accent-bright);
  border-radius: var(--radius);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
  transition: top .28s cubic-bezier(.2, .7, .3, 1), left .28s cubic-bezier(.2, .7, .3, 1),
              width .28s cubic-bezier(.2, .7, .3, 1), height .28s cubic-bezier(.2, .7, .3, 1);
}
@media (prefers-reduced-motion: reduce) { .tour-ring { transition: none; } }

/* The element being explained sits above the dimmer. It only accepts clicks
   when the step says it is meant to be used — otherwise it is shown, not
   operated, and a stray tap cannot navigate away mid-tour. */
/* No `position` here: it used to be `relative`, which overrode the absolute
   positioning of the very element step 2 points at and moved it out of the
   card. app.js adds .is-tour-lift only when the element is static. */
.is-tour-target { z-index: 91; pointer-events: none; }
.is-tour-lift { position: relative; }
.is-tour-clickable { pointer-events: auto; }

.tour-bubble {
  position: fixed; z-index: 92; pointer-events: auto;
  width: min(340px, calc(100vw - 24px));
  background: var(--surface-2);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  box-shadow: 0 20px 50px rgb(0 0 0 / .6);
  padding: 16px 18px 14px;
  transition: top .28s cubic-bezier(.2, .7, .3, 1), left .28s cubic-bezier(.2, .7, .3, 1);
}
@media (prefers-reduced-motion: reduce) { .tour-bubble { transition: none; } }

/* The arrow, pointing back at the hole. Flipped when the bubble had to go
   above its target for want of room below. */
.tour-bubble::before {
  content: ""; position: absolute; left: 50%; transform: translateX(-50%);
  top: -9px; border: 9px solid transparent;
  border-top: 0; border-bottom-color: var(--line-strong);
}
.tour-bubble.is-above::before {
  top: auto; bottom: -9px;
  border-bottom: 0; border-top-color: var(--line-strong);
}

.tour-bubble-head { display: flex; align-items: center; justify-content: space-between; }
.tour-count { color: var(--text-faint); font-size: 12px; font-weight: 700; letter-spacing: .04em; }
.tour-skip {
  background: none; border: 0; cursor: pointer;
  color: var(--text-muted); font: inherit; font-size: 13px; padding: 2px 4px;
}
.tour-skip:hover { color: var(--text); }
.tour-title { font-size: 17px; font-weight: 800; margin: 8px 0 6px; }
.tour-body { color: var(--text-muted); font-size: 14px; line-height: 1.5; margin: 0; }
.tour-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-top: 14px;
}
.tour-dots { display: flex; gap: 6px; }
.tour-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--line-strong); transition: background-color .2s ease;
}
.tour-dot.is-on { background: var(--accent-bright); }

/* Nothing scrolls behind the tour except the tour's own scrollIntoView. */
body.has-tour { overflow: hidden; }

/* ── The reward dialog ───────────────────────────────────────────────────── */
.ob-card { max-width: 440px; text-align: center; padding: 26px 24px 24px; }
.ob-art { display: grid; place-items: center; margin: 4px 0 16px; }
.i-gift { width: 76px; height: 76px; fill: var(--accent-warm); }
.ob-title { font-size: 21px; font-weight: 800; margin: 0 0 8px; }
.ob-body { color: var(--text-muted); line-height: 1.55; margin: 0; }
.ob-foot { margin-top: 22px; display: grid; gap: 12px; justify-items: center; }
.ob-later {
  background: none; border: 0; cursor: pointer;
  color: var(--text-muted); font: inherit; font-size: 14px;
}
.ob-gift-pct {
  font-size: 44px; font-weight: 900; line-height: 1;
  color: var(--accent-warm); margin: 0 0 6px;
}
.ob-gift-expiry {
  display: inline-flex; align-items: center; gap: 7px;
  margin: 14px 0 0; color: var(--accent-warm); font-weight: 700; font-size: 14px;
}
.i-clock { width: 17px; height: 17px; }
.ob-gift-note { color: var(--text-faint); font-size: 13px; margin: 6px 0 0; }

@media (prefers-reduced-motion: no-preference) {
  .ob-step:not([hidden]) { animation: opt-in .26s cubic-bezier(.2, .7, .3, 1) both; }
}

/* ── Language picker ─────────────────────────────────────────────────────── */
.lang-form { position: relative; display: flex; gap: 6px; }

/* The native control stays in the DOM and stays the thing that submits. It is
   only hidden once app.js has confirmed it can replace it — see
   wireLanguagePicker — so a broken script leaves a working <select>. */
.lang-select-native.is-replaced {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.lang-button {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--radius-pill);
  padding: 7px 12px; font: inherit; font-size: 14px; font-weight: 600;
  cursor: pointer;
  transition: border-color .18s ease, background-color .18s ease;
}
.lang-button:hover { border-color: var(--line-strong); background: var(--surface-3); }
.lang-button[aria-expanded="true"] { border-color: var(--accent); }
.i-globe { width: 18px; height: 18px; color: var(--text-muted); }
.lang-button:hover .i-globe, .lang-button[aria-expanded="true"] .i-globe { color: var(--accent-bright); }
.lang-code { letter-spacing: .04em; }
.lang-chevron {
  width: 15px; height: 15px; color: var(--text-faint);
  transition: transform .18s cubic-bezier(.2, .7, .3, 1);
}
.lang-button[aria-expanded="true"] .lang-chevron { transform: rotate(180deg); }

.lang-menu {
  position: absolute; top: calc(100% + 8px); inset-inline-end: 0; z-index: 60;
  min-width: 208px; max-height: 336px; overflow-y: auto;
  background: var(--surface-2);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  box-shadow: 0 18px 46px rgb(0 0 0 / .55);
  padding: 6px;
  /* Closed state. The open class flips all three; transform-origin at the top
     makes it unfold from the button rather than grow from its own middle. */
  opacity: 0; transform: translateY(-6px) scale(.97); transform-origin: top right;
  transition: opacity .16s ease, transform .18s cubic-bezier(.2, .7, .3, 1);
}
[dir="rtl"] .lang-menu { transform-origin: top left; }
.lang-menu.is-open { opacity: 1; transform: none; }

.lang-option {
  display: flex; align-items: center; gap: 10px; width: 100%;
  background: none; border: 0; cursor: pointer;
  color: var(--text-muted); font: inherit; font-size: 14px;
  text-align: start;
  padding: 9px 10px; border-radius: var(--radius-md);
}
.lang-option:hover { background: var(--surface-3); color: var(--text); }
.lang-option.is-current { color: var(--text); font-weight: 700; }
.lang-option-code {
  flex: none; min-width: 2.2em;
  color: var(--text-faint); font-size: 12px; font-weight: 700; letter-spacing: .05em;
}
.lang-option.is-current .lang-option-code { color: var(--accent-bright); }
.lang-option-name { flex: 1; }
.lang-tick { width: 16px; height: 16px; flex: none; color: var(--accent-bright); }

@media (prefers-reduced-motion: reduce) {
  .lang-menu, .lang-chevron { transition: none; }
}

/* ── Profile menu ────────────────────────────────────────────────────────── */
/* The header's one control. It replaced a nav, a dashboard link and a language
   picker standing side by side — four ways in, and the first thing to overflow
   on a phone. */
.usermenu { position: relative; }
.usermenu-button {
  display: grid; place-items: center;
  width: 38px; height: 38px; padding: 0;
  border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--line);
  color: var(--text-muted); cursor: pointer;
  transition: border-color .18s ease, background-color .18s ease, color .18s ease;
}
.usermenu-button:hover,
.usermenu-button[aria-expanded="true"] {
  background: var(--surface-3); border-color: var(--accent); color: var(--text);
}
.usermenu-button .i-person { width: 21px; height: 21px; fill: currentColor; }

.usermenu-panel {
  position: absolute; top: calc(100% + 10px); inset-inline-end: 0; z-index: 70;
  min-width: 236px;
  background: var(--surface-2);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  box-shadow: 0 18px 46px rgb(0 0 0 / .55);
  padding: 6px;
  opacity: 0; transform: translateY(-6px) scale(.97); transform-origin: top right;
  transition: opacity .16s ease, transform .18s cubic-bezier(.2, .7, .3, 1);
}
[dir="rtl"] .usermenu-panel { transform-origin: top left; }
.usermenu-panel.is-open { opacity: 1; transform: none; }

.usermenu-item {
  display: flex; align-items: center; gap: 11px; width: 100%;
  background: none; border: 0; cursor: pointer;
  color: var(--text-muted); font: inherit; font-size: 14.5px; font-weight: 600;
  text-align: start; text-decoration: none;
  padding: 10px 11px; border-radius: var(--radius-md);
}
.usermenu-item:hover { background: var(--surface-3); color: var(--text); }
.usermenu-icon { width: 18px; height: 18px; flex: none; color: var(--text-muted); }
.usermenu-item:hover .usermenu-icon { color: var(--accent-bright); }
.usermenu-item .i-credit { fill: var(--accent-bright); }
.usermenu-sep { height: 1px; background: var(--line); margin: 6px 4px; }

/* The language row, and the list it unfolds. In place rather than a flyout:
   a sideways submenu has nowhere to go on a phone, and this list is twenty
   items long. */
.usermenu-lang { justify-content: flex-start; }
.usermenu-lang-current {
  margin-inline-start: auto;
  color: var(--text-faint); font-size: 12px; font-weight: 700; letter-spacing: .05em;
}
.usermenu-chevron {
  width: 15px; height: 15px; flex: none; color: var(--text-faint);
  transition: transform .18s cubic-bezier(.2, .7, .3, 1);
}
.usermenu-lang[aria-expanded="true"] .usermenu-chevron { transform: rotate(180deg); }

/* Inside the panel the row and its list stack. .lang-form is display:flex from
   the old header, where the control and its fallback button sat side by side —
   left as it was, the list became a flex sibling of the button and was squeezed
   into the 104px left over beside it, with every language name clipped. */
.usermenu-panel .lang-form { display: block; width: 100%; }

/* And the list is a nested block, not a floating card. */
.usermenu-panel .lang-menu {
  position: static; min-width: 0; box-shadow: none;
  border: 0; border-radius: var(--radius-md);
  background: var(--surface); padding: 4px;
  max-height: 232px; margin-top: 4px;
  width: 100%; overflow-x: hidden;
  transform: none; opacity: 1;
}
.usermenu-panel .lang-menu[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .usermenu-panel, .usermenu-chevron, .usermenu-button { transition: none; }
}

/* The ornament beside the annual toggle. Sized here rather than by the
   animation's own 512px canvas, which the player scales to fit. */
.sub-toggle-anim {
  /* Sized to the row it sits in rather than to its own 512px canvas. At 40 it
     was taller than the toggle beside it and read as the subject of the line;
     at 28 it sits with the 14px label as an ornament. */
  width: 28px; height: 28px; flex: none;
  display: inline-block;
}
.sub-toggle-anim svg, .sub-toggle-anim canvas { display: block; width: 100%; height: 100%; }
@media (max-width: 599px) { .sub-toggle-anim { width: 24px; height: 24px; } }

/* The photo/video mark on a catalogue tile. */
.badge-kind { display: inline-flex; align-items: center; gap: 5px; }
.kind-icon { flex: none; color: var(--text); }
/* Two different weights of drawing, so two different sizes.
 *
 * The play triangle is a solid silhouette and the picture frame is outlined in
 * 2px strokes, so at one size the frame reads as the fainter of the two. It
 * gets 14px against the triangle's 11 — matched by how heavy they look, not by
 * the number in the CSS. */
.badge-kind .i-play  { width: 11px; height: 11px; }
.badge-kind .i-photo { width: 14px; height: 14px; }

/* ── The free-trial beacon ───────────────────────────────────────────────── */
/* On the free tile while the trial is untaken and nothing has been paid.
 *
 * A ring plus a bloom rather than a filter on the tile: the tile is a playing
 * video, and anything that repaints its pixels every frame is a frame budget
 * spent on decoration. This animates only box-shadow on a pseudo-element, so
 * it composites and never touches the video. */
.card-beacon { position: relative; }
.card-beacon::after {
  content: "";
  position: absolute; inset: -3px;
  border-radius: calc(var(--radius) + 3px);
  border: 2px solid var(--accent-bright);
  pointer-events: none;
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-bright) 55%, transparent);
}
@media (prefers-reduced-motion: no-preference) {
  .card-beacon::after { animation: beacon 2.4s cubic-bezier(.4, 0, .3, 1) infinite; }
}
@keyframes beacon {
  /* The pause is deliberate: a ring that pulses without stopping stops being
     a signal and becomes wallpaper. One beat, then two seconds of quiet. */
  0%   { box-shadow: 0 0 0 0   color-mix(in srgb, var(--accent-bright) 55%, transparent); }
  18%  { box-shadow: 0 0 0 12px color-mix(in srgb, var(--accent-bright) 0%,  transparent); }
  100% { box-shadow: 0 0 0 12px color-mix(in srgb, var(--accent-bright) 0%,  transparent); }
}
/* Without the pulse the ring alone still marks the tile — the preference asks
   for no motion, not for no signal. */

/* ── Arm c: the persistent reminder ──────────────────────────────────────── */
.trial-strip {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 16px;
  background: color-mix(in srgb, var(--accent) 16%, var(--surface));
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  font-size: 14px; font-weight: 600;
}
.trial-strip-icon { width: 15px; height: 15px; flex: none; color: var(--accent-bright); }
.trial-strip > span { flex: 1; }
.trial-strip-cta {
  color: var(--accent-bright); font-weight: 700; text-decoration: underline;
  text-underline-offset: 3px; white-space: nowrap;
}
.trial-strip-close {
  background: none; border: 0; cursor: pointer; padding: 4px;
  color: var(--text-faint); line-height: 0;
}
.trial-strip-close:hover { color: var(--text); }
.trial-strip-close .i-close { width: 16px; height: 16px; }
@media (max-width: 599px) {
  .trial-strip { font-size: 13px; padding: 8px 12px; gap: 8px; }
}

/* ── Admin ───────────────────────────────────────────────────────────────── */
.adm-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.adm-h2 { font-size: 18px; font-weight: 800; margin: 30px 0 10px; }
.adm-flag {
  font-size: 12px; font-weight: 700; letter-spacing: .05em;
  color: var(--text-faint); text-transform: uppercase;
  border: 1px solid var(--line-strong); border-radius: var(--radius-pill);
  padding: 2px 9px; margin-inline-start: 8px;
}
.adm-cards {
  display: grid; gap: 10px; margin-top: 16px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 700px)  { .adm-cards { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.adm-card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-md); padding: 14px 16px;
}
.adm-card-label { color: var(--text-faint); font-size: 12px; font-weight: 700;
                  letter-spacing: .05em; text-transform: uppercase; margin: 0; }
.adm-card-value { font-size: 24px; font-weight: 800; margin: 4px 0 0; }

.adm-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.adm-table th, .adm-table td {
  text-align: start; padding: 9px 12px; border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.adm-table th { color: var(--text-faint); font-size: 12px; font-weight: 700;
                letter-spacing: .04em; text-transform: uppercase; }
.adm-table tbody tr:hover { background: var(--surface-2); }
/* An error is the one column worth reading in full, so it alone may wrap. */
.adm-error { white-space: normal; color: var(--text-muted); font-size: 13px; }

.alert-warn {
  background: color-mix(in srgb, var(--accent-warm) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-warm) 42%, transparent);
  color: var(--text);
}
.adm-note {
  color: var(--text-muted); font-size: 13.5px; line-height: 1.55;
  margin: 10px 0; max-width: 78ch;
}
.adm-note b { color: var(--text); }

/* The funnel's bar. Inline in the cell beside the number rather than a chart
   of its own: the shape of the drop-off is the thing being read, and it is
   read down a column in one glance. */
.adm-bar {
  display: inline-block; vertical-align: middle;
  width: 90px; height: 8px; margin-inline-end: 8px;
  border-radius: 999px; background: var(--line-strong); overflow: hidden;
}
.adm-bar i {
  display: block; height: 100%;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
}
/* Fill widths as classes. The style attribute that used to do this was blocked
   by the site's own CSP (style-src 'self'), so every bar drew full width and
   the chart quietly agreed with whatever you hoped it said. */
.adm-bar i.w0 { width: 0%; }
.adm-bar i.w5 { width: 5%; }
.adm-bar i.w10 { width: 10%; }
.adm-bar i.w15 { width: 15%; }
.adm-bar i.w20 { width: 20%; }
.adm-bar i.w25 { width: 25%; }
.adm-bar i.w30 { width: 30%; }
.adm-bar i.w35 { width: 35%; }
.adm-bar i.w40 { width: 40%; }
.adm-bar i.w45 { width: 45%; }
.adm-bar i.w50 { width: 50%; }
.adm-bar i.w55 { width: 55%; }
.adm-bar i.w60 { width: 60%; }
.adm-bar i.w65 { width: 65%; }
.adm-bar i.w70 { width: 70%; }
.adm-bar i.w75 { width: 75%; }
.adm-bar i.w80 { width: 80%; }
.adm-bar i.w85 { width: 85%; }
.adm-bar i.w90 { width: 90%; }
.adm-bar i.w95 { width: 95%; }
.adm-bar i.w100 { width: 100%; }

/* A branch row describes the people in the row above; it is not a stage
   everybody passed through. Indented and muted so the eye reads the spine of
   the funnel straight down the bold labels and never adds a branch into it. */
.adm-branch {
  color: var(--text-muted);
  margin-inline-end: 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.adm-sub { display: block; color: var(--text-muted); font-size: 12px; }
/* Where two different populations meet: browsers above, accounts below. */
.adm-join td { border-top: 2px solid var(--line-strong); }

/* ── Deleting an account ─────────────────────────────────────────────────────
   Folded, muted, and the only red thing on the site. Everything destructive
   should be reachable without being easy: the summary is a plain line, the
   button behind it is unmistakable, and there is a word to type between them. */
.danger-zone {
  margin-top: 40px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 16px;
}
.danger-zone > summary {
  cursor: pointer;
  color: var(--text-dim);
  font-size: 14px;
  list-style: none;
}
.danger-zone > summary::-webkit-details-marker { display: none; }
.danger-zone > summary:hover { color: var(--text); }
.danger-zone[open] > summary { margin-bottom: 10px; color: var(--text); }
.danger-form { display: grid; gap: 10px; max-width: 320px; }
.btn-danger {
  background: #b3261e;
  border-color: #b3261e;
  color: #fff;
  justify-self: start;
}
.btn-danger:hover { background: #c9352c; border-color: #c9352c; }

/* Said before the upload, not after it: the plan a mode needs, a spent free
   generation, a balance that will not cover the job. Each one replaces the
   Generate button rather than sitting beside it — a button that is going to
   be refused should not be there to press. */
.gen-block {
  display: grid; gap: 10px; justify-items: start;
  margin-top: 14px; padding: 14px 16px;
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  background: var(--surface-2);
}
.gen-block p { margin: 0; color: var(--text-dim); font-size: 14px; }

/* The two documents the age gate asks people to accept. Small, but reachable —
   agreeing to something you cannot open is not agreement. */
.gate-links {
  display: flex; gap: 18px; justify-content: center;
  margin: 0 0 14px; font-size: 13px;
}
.gate-links a { color: var(--text-dim); text-decoration: underline; }
.gate-links a:hover { color: var(--text); }

/* ── The advertisement on the clean storefront ───────────────────────────────
   Visibly an ad in every placement. It sits where a missing feature is looked
   for, which is exactly why it must not be able to pass for a feature: the
   mark is not decoration, it is what keeps a listing from becoming a
   complaint. See templates/_ad.html. */
.offsite-mark {
  display: inline-block;
  padding: 1px 6px; border-radius: 4px;
  background: var(--line-strong); color: var(--text-dim);
  font-size: 10px; font-weight: 700; letter-spacing: .06em;
}
.offsite-tile {
  display: flex; flex-direction: column; gap: 10px;
  align-items: flex-start; justify-content: center;
  padding: 18px; text-decoration: none;
  border: 1px dashed var(--line-strong);
  background: var(--surface-2);
}
.offsite-tile-body { display: grid; gap: 4px; }
.offsite-tile b { color: var(--text); font-size: 16px; line-height: 1.25; }
.offsite-sub { color: var(--text-dim); font-size: 13px; }
.offsite-go {
  align-self: flex-start;
  padding: 7px 14px; border-radius: 999px;
  background: var(--accent); color: #fff;
  font-size: 13px; font-weight: 600;
}
.offsite-tab {
  display: flex; align-items: center; gap: 8px;
  margin-top: 14px; padding: 10px 14px;
  border: 1px dashed var(--line-strong); border-radius: var(--radius);
  color: var(--text-dim); text-decoration: none; font-size: 14px;
}
.offsite-tab:hover { color: var(--text); border-color: var(--accent); }
/* The top-of-page advertisement card.
 *
 * Every number here was read off the reference site's own computed styles, not
 * eyeballed from a screenshot: 18px corners, a 135px media column, a 122px
 * card the taller clip is cropped by, max-width 640px, and the three-part
 * shadow (drop, rim light, inner glow) that makes it sit above the page
 * instead of on it.
 *
 * It replaces a dashed grey strip with no picture in it. The placement is
 * worth having only because it SHOWS what this site cannot do, and text
 * shows nothing. */
.offsite-card {
  position: relative;
  display: grid;
  /* Wider than their 176px and taller than their 136px, by owner decision:
     their demo clip is a face close-up that survives a letterbox crop, ours is
     a standing full-body shot and at 135x122 she barely fitted. Every pixel
     added here is one less pixel cropped off her. */
  /* A FIXED panel width, deliberately.
     Deriving it from the height with aspect-ratio and an `auto` track is the
     tidier idea and it does not work: the track width then depends on the
     item width, which depends on the item height, which depends on the row —
     a circle the browser is free to resolve either way. It gave 200px on one
     machine and collapsed to 88px on the server, and the shape check passed
     both times because the shape was right and only the SIZE was wrong.
     Taller, never narrower: narrowing the panel cannot show more of the
     subject, because the crop is baked into the file. See CROP_H in
     webapp/tools/make_ad_montage.py. */
  grid-template-columns: minmax(0, 1fr) 200px;
  /* min-height, not their fixed height: their headline is one English line at
     13px and ours is translated into twenty languages, where German runs to
     three. A fixed height would clip the words instead of the clip. */
  /* A FIXED height, not a minimum.
     The panel beside it is a fixed width, so any card that grows taller than
     185px puts the two out of shape and CSS starts trimming the montage's
     sides — which is precisely the cropping this change set out to remove. A
     minimum let twenty translations each pick their own height; chasing that
     with font sizes moved the problem between breakpoints instead of solving
     it. The height is now the same number the montage was encoded for, and the
     headline gives way instead (clamped below). */
  height: 185px;
  max-width: 640px;
  margin-inline: auto;
  overflow: hidden;
  border-radius: 22px;
  text-decoration: none;
  background:
    radial-gradient(84% 115% at 38% 0%, rgb(37 99 176 / .26) 0%, rgb(3 13 27 / 0) 54%),
    linear-gradient(135deg, #06152b 0%, #081d36 48%, #02060c 100%);
  box-shadow:
    0 24px 42px rgb(0 0 0 / .28),
    inset 0 1px 0 rgb(151 197 255 / .10),
    inset 0 0 38px rgb(31 105 200 / .16);
}
.offsite-card-text {
  display: flex; flex-direction: column; gap: 6px;
  /* min-height: 0 is what makes the card's fixed height real. A grid item
     defaults to min-height: auto, so the row cannot be shorter than the text
     inside it — the card was set to 137px and came out 146px, and the panel
     beside it went out of shape by exactly that much. */
  justify-content: center; min-width: 0; min-height: 0;
  /* Logical, not physical. In Arabic the grid puts the clip on the left, and
     a hard-coded left pad would leave the words jammed against it. */
  padding: 14px; padding-inline-start: 22px; padding-inline-end: 10px;
  gap: 8px;
}
/* The mark keeps its pill and its contrast. Theirs is 10px of rgb(37 95 168 /
   .42) on near-black, which is a disclosure in name only. */
.offsite-card .offsite-mark {
  align-self: flex-start;
  background: rgb(255 255 255 / .16); color: #eaf2ff;
}
.offsite-card-title {
  color: #fff; font-size: 15px; font-weight: 800; line-height: 1.15;
  /* Two lines, then an ellipsis. A headline losing a word is a smaller price
     than the advertisement's picture losing its sides, and unlike the picture
     it is a thing a translator can shorten. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.offsite-card-sub {
  color: rgb(213 231 255 / .75); font-size: 12px; font-weight: 500;
  line-height: 1.2;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1;
  overflow: hidden;
}
.offsite-card-go {
  align-self: flex-start;
  display: inline-flex; align-items: center; gap: 4px;
  height: 28px; padding: 0 12px; border-radius: 999px;
  /* Colour from our palette, geometry and lighting from theirs: a white
     top-down wash over the fill, a coloured drop, and a bright inner lip along
     the bottom edge — that last one is what makes it read as a physical pill
     rather than a coloured rectangle. */
  background-color: var(--accent);
  background-image: linear-gradient(rgb(255 255 255 / .18), rgb(255 255 255 / 0));
  color: #fff; font-size: 13px; font-weight: 800;
  /* No `nowrap`, and a ceiling. The text track is minmax(0, 1fr): a pill that
     cannot wrap and cannot shrink simply overflows it — and because the media
     column is positioned and the text column is not, the overflow paints UNDER
     the video and the card's overflow:hidden eats the rest. At 320px the
     French "Essayer gratuitement" lost its arrow and half its right cap that
     way. Wrapping to two lines is not pretty; a call to action with the end
     sliced off is worse. */
  max-width: 100%;
  box-shadow:
    0 5px 12px rgb(47 130 255 / .22),
    inset 0 1px 2px rgb(255 255 255 / .2),
    inset 0 -8px 12px -8px rgb(177 217 255 / .32);
}
/* The shared chevron points down, for the disclosures it was drawn for.
   Here it is a "keep going" arrow, so it turns — and it turns the other way in
   Arabic, where forward is to the left. */
.offsite-go-chevron {
  width: 13px; height: 13px; flex: 0 0 auto;
  transform: rotate(-90deg);
}
[dir="rtl"] .offsite-go-chevron { transform: rotate(90deg); }

.offsite-card-art {
  position: relative; overflow: hidden;
  /* 200 / 185 matches OUT_W / OUT_H in make_ad_montage.py, so at the card's
     natural height the montage is shown whole. A translation that needs a
     second line makes the card taller and CSS then trims the montage's sides —
     under 9% on a phone, and the alternative was the collapse above. */
  /* The slanted edge, measured off theirs: the panel's left side leans, cut
     from 20% of the way in at the top down to flush at the bottom. It is a
     clip-path on the panel, not a rotated overlay, so the card's own gradient
     shows through the missing corner and there is nothing to keep in sync
     when the card changes height. */
  clip-path: polygon(20% 0, 100% 0, 100% 100%, 0 100%);
}
.offsite-card-clip {
  /* Absolute, and this is the whole trick. The clip is a 720x1137 portrait:
     laid out normally it is 213px tall in a 135px column and it, not the
     words, would decide how tall the card is — which is what happened, and
     the card came out 213px deep and ran into the strip below it.
     Taken out of flow it contributes no height at all, so the TEXT sets the
     card's height (min 122px, more when a translation needs it) and the clip
     always fills exactly that, cropped. Their card pins a fixed 122px to get
     the same effect and clips its own headline if it ever wraps. */
  position: absolute; inset: 0;
  display: block; width: 100%; height: 100%;
  /* Chosen by rendering the candidates and looking at them, not by copying
     their number. Theirs sits at 42% because their clip is a face close-up;
     ours is a standing full-body shot, and at 38% the crop began below the
     chin — a faceless torso, which is what "she barely fits" meant. At 12%
     the face is in frame at every second of the clip AND the undressing, which
     is the thing being advertised, is still visible. */
  object-fit: cover; object-position: 50% 12%;
}

/* Two columns on a phone is 135px of video against ~180px of text, and the
   headline is the part that has to survive. */
@media (max-width: 460px) {
  /* A phone card is ~358px wide. 200px of video would leave the headline
     about 130px, so the panel gives ground first — but not back to 108px,
     which is what made her unreadable in the first place. */
  /* 158 / 146 is the same shape as 200 / 185, and 146 is the height the
     longest translations actually want on a phone — so almost nothing is
     clamped and the montage is never trimmed. */
  .offsite-card { grid-template-columns: minmax(0, 1fr) 158px; height: 146px; }
  /* One line, always, on every phone width — and the arrow goes to pay for it.
     The pill has a fixed height; a label that wraps inside it does not make it
     taller, it spills its second line out through the top and bottom. That is
     what "Essayer gratuitement" and "Kostenlos testen" were doing at 366 and
     390px. Without the arrow the widest label is 168px against a 176px column
     at the tightest width. */
  .offsite-card-go { white-space: nowrap; font-size: 12px; }
  .offsite-go-chevron { display: none; }
}

/* The narrowest phones. Not a fix for the cropping — the fixed height is that —
   but 15px over a 130px column is two words a line. */
@media (max-width: 365px) {
  /* The panel gives way here, and only here. On a 288px card a 158px panel
     leaves 112px of text — not enough for "Essayer gratuitement", which then
     had its own pill clip it. 120x111 is the same shape (1.081) so the montage
     is still shown whole, and it hands 38px back to the words. */
  .offsite-card { grid-template-columns: minmax(0, 1fr) 120px; height: 111px; }
  .offsite-card-title { font-size: 13px; line-height: 1.1; }
  .offsite-card-text { padding-inline-start: 12px; gap: 5px; }
  /* The second line goes. In German at 320px the text column is 112px wide:
     the sub ellipsised down to "Foto hochladen •…", which says nothing, while
     costing the room the button needed — and the button then wrapped onto two
     lines and was cut off by the card's bottom edge. A headline and a button
     that both fit beat three things that do not. */
  .offsite-card-sub { display: none; }
  /* nowrap and no arrow. The column is 112px here: "Kostenlos testen" wrapped
     onto two lines inside a 26px pill and spilled over the headline above it.
     Measured without the chevron it is 104px, so it fits on one line — and a
     button that cannot wrap cannot do that again. */
  .offsite-card-go {
    font-size: 11px; padding: 0 8px; height: 26px; white-space: nowrap;
  }
  .offsite-go-chevron { display: none; }
  .offsite-go-chevron { display: none; }
  .offsite-card-text {
    padding: 10px; padding-inline-start: 12px; padding-inline-end: 6px;
  }
  .offsite-card-title { font-size: 14px; }
  /* One point of type and two of padding is enough to keep every one of the
     twenty translations on a single line at 360px. */
  .offsite-card-go { font-size: 12px; padding: 0 10px; }
}

/* The picker: the customer's own generated pictures, and one more option. */
.picker { margin: 4px 0 10px; }
.picker-row {
  display: flex; gap: 10px; overflow-x: auto; padding-bottom: 6px;
  scroll-snap-type: x mandatory;
}
.picker-item {
  flex: 0 0 auto; width: 96px; aspect-ratio: 3 / 4;
  padding: 0; border: 2px solid transparent; border-radius: 12px;
  overflow: hidden; background: var(--surface-2); cursor: pointer;
  scroll-snap-align: start;
}
.picker-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.picker-item.is-picked { border-color: var(--accent); }

/* The ad shaped like one of them, so it reads as another option rather than
   an interruption — dashed, so it never quite passes for one. */
.picker-offsite {
  display: flex; flex-direction: column; justify-content: center; gap: 6px;
  width: 132px; padding: 10px; text-decoration: none;
  border: 1px dashed var(--line-strong);
}
.picker-offsite-body { display: grid; gap: 3px; }
.picker-offsite b { color: var(--text); font-size: 12px; line-height: 1.3; }
.picker-offsite .offsite-sub { font-size: 11px; }
.picker-offsite:hover { border-color: var(--accent); }

.offsite-rail { margin-top: 14px; margin-bottom: 16px; }

/* The ad the signed-out visitor sees. Most people arriving from a directory
   listing never make an account, so this is the only placement that reaches
   them — and it is a link, not an upload control, for the reason in _ad.html. */
.offsite-guest {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 12px; padding: 12px 14px;
  border: 1px dashed var(--line-strong); border-radius: var(--radius);
  background: var(--surface-2); text-decoration: none;
}
.offsite-guest-body { display: grid; gap: 3px; flex: 1 1 160px; }
.offsite-guest b { color: var(--text); font-size: 15px; }

/* Says an instance is a preview. Loud on purpose: it is served without TLS and
   its data is throwaway, and it otherwise looks exactly like the product. */

/* The picker, closed until asked for. */
.picker-sheet { border: 1px solid var(--line); border-radius: var(--radius); }
.picker-open {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px; cursor: pointer; list-style: none;
  color: var(--text); font-weight: 600;
}
.picker-open::-webkit-details-marker { display: none; }
.picker-open .disclosure-chevron { margin-inline-start: auto; }
.picker-sheet[open] .picker-open { border-bottom: 1px solid var(--line); }
.picker-sheet > :not(summary) { padding-inline: 14px; }
.picker-yours { margin-top: 12px; }

/* The advertisement at the top of the opened picker: the highest-intent
   placement on the site, so it gets room and the clip plays beside it. */
.offsite-lead {
  display: flex; align-items: stretch; gap: 14px;
  margin-top: 14px; padding: 14px;
  border: 1px dashed var(--line-strong); border-radius: var(--radius);
  background: var(--surface-2); text-decoration: none;
}
.offsite-lead-text { display: grid; gap: 6px; align-content: center; flex: 1 1 auto; }
.offsite-lead-text b { color: var(--text); font-size: 17px; line-height: 1.25; }
.offsite-lead-text .offsite-mark { justify-self: start; }
.offsite-lead-art {
  flex: 0 0 auto; width: 92px; aspect-ratio: 3 / 4;
  border-radius: 12px; overflow: hidden; background: var(--surface);
}
.offsite-lead-clip { width: 100%; height: 100%; object-fit: cover; display: block; }
@media (max-width: 520px) {
  .offsite-lead { flex-direction: column; }
  .offsite-lead-art { width: 100%; aspect-ratio: 16 / 9; }
}

/* ── The generator, on the home page ─────────────────────────────────────────
   The clean storefront's first screen contains the whole product: the ad, the
   headline, the tabs, the box you type into and the priced button. Somebody
   arriving from a directory listing was told this is a generator; making them
   pick a tile and open a sheet first is three steps before anything happens. */
.studio { padding: 26px 0 8px; }
.studio-wrap { display: grid; gap: 14px; max-width: 860px; }
.studio-title { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.1; }
.studio-form { display: grid; gap: 12px; }

.studio-tabs {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
  padding: 6px; border-radius: 999px; background: var(--surface-2);
}
.studio-tab {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 11px 16px; border-radius: 999px;
  color: var(--text-dim); text-decoration: none; font-weight: 600; font-size: 15px;
}
.studio-tab.is-on { background: var(--accent); color: #fff; }
.studio-tab:not(.is-on):hover { color: var(--text); }
.studio-tab svg { width: 18px; height: 18px; }

.studio-box {
  position: relative;
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  background: var(--surface-2); padding: 14px 14px 52px;
}
.studio-input {
  width: 100%; border: 0; background: transparent; resize: vertical;
  color: var(--text); font: inherit; font-size: 16px; outline: none;
}
.studio-input::placeholder { color: var(--text-dim); }

/* The ad sits in the corner of the box, where the reference site puts its
   "Undress Photo" — beside what you are typing, for the person who wanted a
   photograph instead of a sentence. */
/* Shaped off the reference site's own chip, measured: a soft navy plate with a
   thin cool-blue rim and cool-blue text, 8px corners rather than a pill, and a
   glyph leading the words. It was a grey dashed outline, which read as a
   disabled control rather than as somewhere to go. */
.offsite-chip {
  position: absolute; inset-inline-end: 12px; bottom: 12px;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 9px 5px 7px; border-radius: 8px;
  border: 1px solid rgb(151 197 255 / .24);
  background-color: #132f5d;
  background-image: linear-gradient(110deg, rgb(255 255 255 / 0) 0%,
                                    rgb(151 197 255 / .05) 100%);
  color: #97c5ff; text-decoration: none; font-size: 13px; font-weight: 500;
}
.offsite-chip-icon { width: 17px; height: 17px; flex: none; }
.offsite-chip .offsite-mark {
  background: rgb(151 197 255 / .18); color: #cfe3ff;
}
.offsite-chip:hover { border-color: rgb(151 197 255 / .5); color: #cfe3ff; }

.studio-go {
  justify-content: center; gap: 8px; padding: 14px 20px; font-size: 16px;
}
@media (max-width: 560px) {
  .studio-box { padding-bottom: 56px; }
  .offsite-chip { inset-inline-end: 10px; bottom: 10px; font-size: 12px; }
}

/* ── The first-visit advertisement ───────────────────────────────────────────
   Measured off the reference site and rebuilt: the clip fills the card, the
   words lie on top of it, the button sits over the bottom, and the whole card
   is the link. Their card is a fixed 285x372; ours takes its shape from the
   clip, so a preview of any proportion sits right. */
.offsite-over {
  position: fixed; inset: 0; z-index: 3000;
  width: 100%; height: 100%; max-width: none; max-height: none;
  display: flex; align-items: center; justify-content: center;
  padding: 20px; border: 0; background: rgb(0 0 0 / .76);
  animation: offsite-over-in .22s ease both;
}
/* Without this the overlay CANNOT be closed with JavaScript blocked, and the
   whole no-JS story of this component is false.
   The close button is a <button type="submit"> in a <form method="dialog">,
   whose native behaviour is to drop the `open` attribute — and nothing more.
   What actually hides a closed dialog is the user-agent rule
   `dialog:not([open]) { display: none }`, and the `display: flex` above is an
   author declaration, which beats the user agent's every time. So `open` came
   off and a fixed, z-index 3000, 76%-black sheet stayed painted over the page,
   still eating every click, with no way left to dismiss it.
   The sibling .modal has no `display` of its own, which is exactly why it has
   never had this bug. */
.offsite-over:not([open]) { display: none; }
.offsite-over::backdrop { background: rgb(0 0 0 / .76); }
body.has-offsite-over { overflow: hidden; }

/* Opening and closing.
 *
 * Two animations, not one on a wrapper: the backdrop only fades, while the
 * card also rises and settles. Moving them together makes the whole screen
 * lurch; moving only the card is what reads as the card arriving.
 *
 * The card's curve overshoots slightly on the way in (cubic-bezier with a y
 * above 1) and leaves on a plain ease-in, because an exit that bounces reads
 * as a mistake rather than as speed. */
@keyframes offsite-over-in    { from { opacity: 0 } to { opacity: 1 } }
@keyframes offsite-over-out   { from { opacity: 1 } to { opacity: 0 } }
@keyframes offsite-card-anim-in {
  from { opacity: 0; transform: translateY(14px) scale(.94); }
  to   { opacity: 1; transform: none; }
}
@keyframes offsite-card-anim-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(8px) scale(.96); }
}

.offsite-over .offsite-over-card {
  animation: offsite-card-anim-in .34s cubic-bezier(.2, .9, .28, 1.06) both;
}
.offsite-over.is-closing { animation: offsite-over-out .2s ease both; }
.offsite-over.is-closing .offsite-over-card {
  animation: offsite-card-anim-out .2s ease-in both;
}

/* Reduced motion still needs the animationend the script waits on, or the
   overlay would hang until the backstop timer fires. So it keeps an
   animation — an instant opacity one, with no movement in it. */
@media (prefers-reduced-motion: reduce) {
  .offsite-over,
  .offsite-over .offsite-over-card,
  .offsite-over.is-closing,
  .offsite-over.is-closing .offsite-over-card {
    animation-duration: .01ms;
    animation-timing-function: linear;
  }
  @keyframes offsite-card-anim-in  { from { opacity: 0 } to { opacity: 1 } }
  @keyframes offsite-card-anim-out { from { opacity: 1 } to { opacity: 0 } }
}

.offsite-over-card {
  /* No fixed proportion. The clip is a normal block inside the card, so the
     card comes out exactly the shape of the clip — a portrait preview gives a
     portrait card and a wide one gives a wide card, with no letterboxing and
     nothing cropped. The reference site pins 285x372 and crops whatever it is
     given; this shows the whole frame, which is the thing being sold.
     min-height covers the case where there is no clip at all. */
  position: relative; overflow: hidden;
  width: min(320px, 100%); min-height: 220px;
  max-height: min(78vh, 640px);
  border-radius: 22px; background: var(--surface-2);
  box-shadow: 0 24px 70px rgb(0 0 0 / .6);
}
.offsite-over-clip {
  display: block; width: 100%; height: auto;
  max-height: min(78vh, 640px); object-fit: cover;
}
/* The whole card, clickable, under the controls that sit on top of it. */
.offsite-over-hit { position: absolute; inset: 0; z-index: 1; }

/* Both bands carry a scrim. Not decoration: white text over video is
   unreadable the moment a bright frame comes round. */
.offsite-over-card::before,
.offsite-over-card::after {
  content: ""; position: absolute; inset-inline: 0; z-index: 2;
  pointer-events: none;
}
/* Tight, and only where the words are. Two 45% bands at .78 covered ninety
   per cent of the frame between them and the clip read as dimmed — which is
   the opposite of what an advertisement wants. These reach just past the text
   they exist for and fall off fast, so the middle of the frame is untouched. */
.offsite-over-card::before {
  top: 0; height: 32%;
  background: linear-gradient(to bottom,
    rgb(0 0 0 / .72) 0%, rgb(0 0 0 / .34) 55%, transparent 100%);
}
.offsite-over-card::after {
  bottom: 0; height: 30%;
  background: linear-gradient(to top,
    rgb(0 0 0 / .72) 0%, rgb(0 0 0 / .30) 60%, transparent 100%);
}

.offsite-over-top {
  position: absolute; z-index: 3; inset-inline: 0; top: 0;
  display: grid; gap: 4px; justify-items: center; text-align: center;
  padding: 16px 44px 0; pointer-events: none;
}
.offsite-over-top .offsite-mark { background: rgb(255 255 255 / .16); color: #fff; }
.offsite-over-title {
  margin: 4px 0 0; color: #fff; font-size: 19px; font-weight: 800; line-height: 1.2;
  text-shadow: 0 2px 12px rgb(0 0 0 / .6);
}
.offsite-over-sub {
  margin: 0; color: rgb(255 255 255 / .84); font-size: 13px; font-weight: 500;
  text-shadow: 0 2px 10px rgb(0 0 0 / .6);
}

.offsite-over-go {
  position: absolute; z-index: 3; bottom: 16px; left: 50%;
  transform: translateX(-50%);
  min-width: 62%; padding: 12px 22px; border-radius: 999px;
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
  color: #fff; text-align: center; text-decoration: none;
  font-size: 14px; font-weight: 800;
  box-shadow: 0 8px 24px rgb(0 0 0 / .45);
}
.offsite-over-go:hover { filter: brightness(1.06); }

.offsite-over-dismiss { position: absolute; z-index: 4; top: 10px; inset-inline-end: 10px; margin: 0; }
.offsite-over-close {
  width: 30px; height: 30px; display: grid; place-items: center;
  border: 0; border-radius: 50%;
  background: rgb(0 0 0 / .45); color: #fff; cursor: pointer;
}
.offsite-over-close:hover { background: rgb(0 0 0 / .7); }
.offsite-over-close svg { width: 16px; height: 16px; }

/* The picker, closed until asked for. */
.picker-sheet { border: 1px solid var(--line); border-radius: var(--radius); }
.picker-open {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px; cursor: pointer; list-style: none;
  color: var(--text); font-weight: 600;
}
.picker-open::-webkit-details-marker { display: none; }
.picker-open .disclosure-chevron { margin-inline-start: auto; }
.picker-sheet[open] .picker-open { border-bottom: 1px solid var(--line); }
.picker-sheet > :not(summary) { padding-inline: 14px; }
.picker-yours { margin-top: 12px; }

/* The advertisement at the top of the opened picker: the highest-intent
   placement on the site, so it gets room and the clip plays beside it. */
.offsite-lead {
  display: flex; align-items: stretch; gap: 14px;
  margin-top: 14px; padding: 14px;
  border: 1px dashed var(--line-strong); border-radius: var(--radius);
  background: var(--surface-2); text-decoration: none;
}
.offsite-lead-text { display: grid; gap: 6px; align-content: center; flex: 1 1 auto; }
.offsite-lead-text b { color: var(--text); font-size: 17px; line-height: 1.25; }
.offsite-lead-text .offsite-mark { justify-self: start; }
.offsite-lead-art {
  flex: 0 0 auto; width: 92px; aspect-ratio: 3 / 4;
  border-radius: 12px; overflow: hidden; background: var(--surface);
}
.offsite-lead-clip { width: 100%; height: 100%; object-fit: cover; display: block; }
@media (max-width: 520px) {
  .offsite-lead { flex-direction: column; }
  .offsite-lead-art { width: 100%; aspect-ratio: 16 / 9; }
}

/* ── The generator, on the home page ─────────────────────────────────────────
   The clean storefront's first screen contains the whole product: the ad, the
   headline, the tabs, the box you type into and the priced button. Somebody
   arriving from a directory listing was told this is a generator; making them
   pick a tile and open a sheet first is three steps before anything happens. */
.studio { padding: 26px 0 8px; }
.studio-wrap { display: grid; gap: 14px; max-width: 860px; }
.studio-title { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.1; }
.studio-form { display: grid; gap: 12px; }

.studio-tabs {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
  padding: 6px; border-radius: 999px; background: var(--surface-2);
}
.studio-tab {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 11px 16px; border-radius: 999px;
  color: var(--text-dim); text-decoration: none; font-weight: 600; font-size: 15px;
}
.studio-tab.is-on { background: var(--accent); color: #fff; }
.studio-tab:not(.is-on):hover { color: var(--text); }
.studio-tab svg { width: 18px; height: 18px; }

.studio-box {
  position: relative;
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  background: var(--surface-2); padding: 14px 14px 52px;
}
.studio-input {
  width: 100%; border: 0; background: transparent; resize: vertical;
  color: var(--text); font: inherit; font-size: 16px; outline: none;
}
.studio-input::placeholder { color: var(--text-dim); }

.studio-go {
  justify-content: center; gap: 8px; padding: 14px 20px; font-size: 16px;
}
@media (max-width: 560px) {
  .studio-box { padding-bottom: 56px; }
  .offsite-chip { inset-inline-end: 10px; bottom: 10px; font-size: 12px; }
}


/* ── While the picture is still on its way ───────────────────────────────────
   The blurred thumbnail is inlined in the markup, so it paints on the first
   frame with nothing fetched. On its own it reads as a broken image; moving,
   it reads as a picture arriving. That is the whole job — it is removed the
   moment anything sharper is on top of it, because a shimmer that never stops
   is worse than none.

   Honoured for people who asked for less motion: they get the blur, still. */
.card-lqip::after,
.card-media > .card-shimmer {
  content: "";
  position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(105deg,
    transparent 35%,
    rgb(255 255 255 / .09) 48%,
    rgb(255 255 255 / .16) 50%,
    rgb(255 255 255 / .09) 52%,
    transparent 65%);
  background-size: 260% 100%;
  animation: card-shimmer 1.9s linear infinite;
  pointer-events: none;
}

/* Gone as soon as ANY sharp layer has decoded — the still on a video card,
   the photograph on a photo card, or the clip itself.
 *
 * It named .card-poster and .card-clip only, and a photo mode has neither, so
 * every photo tile shimmered over a picture that had finished loading. The
 * rule is not "the poster arrived", it is "something better than the blur is
 * here", and it has to be written that way or the next layer added will
 * reintroduce the same bug. */
.card-media:has(.card-img.is-loaded) > .card-shimmer,
.card-media:has(.card-clip[src]) > .card-shimmer,
.card-source:has(.card-source-img.is-loaded) > .card-shimmer { display: none; }

@keyframes card-shimmer {
  from { background-position: 160% 0; }
  to   { background-position: -60% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .card-media > .card-shimmer { animation: none; opacity: 0; }
}


/* The source thumbnail shimmers on its own. It is a separate element loading
   separately, so without this it is the one part of the card that goes from
   empty straight to filled. */
.card-source > .card-shimmer { z-index: 1; border-radius: inherit; }
.card-source-img { position: relative; z-index: 2; }

/* ── Recharge ────────────────────────────────────────────────────────────────
   A fourth card under the three, centred and in its own colour: not a fourth
   tier, the one-off top-up. Green because it is the only thing on this page
   that is not a subscription, and it should not be mistaken for one. */
.sub-recharge-row { display: flex; justify-content: center; margin-top: 18px; }
.sub-card-recharge {
  width: min(360px, 100%);
  background: linear-gradient(160deg, #1f7a5a, #14614a);
  border-color: rgb(255 255 255 / .14);
}
.sub-card-recharge .sub-name { color: #fff; }
.sub-card-recharge .sub-price b { color: #fff; }

/* The recharge sheet. */
.topup-modal .field { display: grid; gap: 6px; margin-bottom: 6px; }
.topup-input { font-size: 20px; font-weight: 700; }
.topup-outcome { margin: 0 0 12px; color: var(--text-dim); font-size: 14px; }
.topup-rails { display: grid; gap: 10px; margin-top: 14px; }


/* ── The card caption ────────────────────────────────────────────────────────
   Title centred at the bottom with its marks under it, over a scrim that
   covers only the bottom. The arrangement the reference site uses, and better
   than the corner badges it replaces: a tile is read as a picture with a
   caption, and a caption belongs under the picture rather than scattered into
   three corners.

   The scrim stops where the words stop. Darkening a whole tile to make one
   line legible spends the picture to save the caption. */
.card-overlay {
  position: absolute; inset-inline: 0; bottom: 0; top: auto; z-index: 3;
  display: grid; gap: 8px; justify-items: center;
  padding: 40px 10px 12px;
  text-align: center;
  pointer-events: none;
  background: linear-gradient(to top,
    rgb(0 0 0 / .82) 0%, rgb(0 0 0 / .5) 45%, transparent 100%);
}
.card-title {
  margin: 0;
  font-size: 17px; font-weight: 800; line-height: 1.15; color: #fff;
  text-shadow: 0 2px 10px rgb(0 0 0 / .65);
}
.card-marks { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }

/* The marks. Each says one thing, and each is its own pill so they can be
   scanned rather than parsed. */
.card-marks .badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px; border-radius: 999px;
  font-size: 12px; font-weight: 700; letter-spacing: .02em;
}
/* The glyph carries the meaning faster than the word does once you know the
   site, so it is allowed to outweigh it slightly. */
.card-marks .kind-icon { width: 15px; height: 15px; }
/* Scoped. Unscoped, this beat the gradient .badge-free defined earlier and
   silently restyled the gallery's badge too — the third time a duplicate rule
   in this file has won on source order. */
.card-marks .badge-free  { background: var(--accent); color: #fff; }
.card-marks .badge-price { background: rgb(0 0 0 / .55); color: #fff; }
/* Paid feature. Reads at a glance against the blue tiles, which is its
   entire job — the price pill next to it says how much, this says what kind. */
.card-marks .badge-paid {
  background: linear-gradient(135deg, var(--paid), var(--paid-2));
  color: #fff;
}

/* Two phone columns leave a card 176px wide, and the marks have to hold four
   pills. English fits at the full size; German does not — "KOSTENPFLICHTIG"
   alone measured 143px and pushed the row to a third line, which grows the
   scrim and eats the picture the tile is selling. Tightening every pill by two
   pixels of padding and one of type buys that line back in every language,
   rather than shortening one word into something German does not say. */
@media (max-width: 460px) {
  .card-marks .badge { padding: 4px 8px; font-size: 11px; gap: 4px; }
  .card-marks { gap: 5px; }
}
.badge-kind  { background: rgb(255 255 255 / .16); color: #fff; }
/* How long the clip is, in its own pill: it is the thing modes are compared
   on, and it was a number tucked inside the kind badge. */
.badge-duration { background: rgb(0 0 0 / .62); color: #fff; }

/* ── The source thumbnail ────────────────────────────────────────────────────
   Top-left and tilted, the way the reference site does it. The tilt is what
   makes it read as a photograph laid on top of the tile rather than as a
   second picture cropped into the corner — and that is the whole job of it,
   because it answers "what do I put in" before a word is read. */
.card-source {
  position: absolute; inset-inline-start: 10px; top: 10px;
  inset-inline-end: auto; bottom: auto;
  margin: 0; z-index: 3;
  width: 30%; max-width: 88px;
  border-radius: 8px;
  overflow: hidden;
  border: 2px solid rgb(255 255 255 / .9);
  box-shadow: 0 6px 16px rgb(0 0 0 / .5);
  transform: rotate(-4deg);
}
