/* ══════════════════════════════════════════════════════════════════════
   Convertlab
   One layout (B · One-Box) and one theme. The A/C themes and layouts were
   removed on 2026-10-03; C's result table survives inside B's queue panel.
   Ad slots are real components so we can prove the layout survives them
   being removed.
   ══════════════════════════════════════════════════════════════════════ */

/* ── tokens ──────────────────────────────────────────────────────────── */
:root {
  --f-sans: ui-sans-serif, -apple-system, "Segoe UI", Inter, Roboto, sans-serif;
  --f-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 18px;
  --r-xl: 26px;
  --dur: .18s;

  --bg: #fbfbfd;
  --bg-grad: linear-gradient(180deg, #f4f4f8 0%, #fbfbfd 340px);
  --panel: #ffffff;
  --panel-2: #f6f7fa;
  --border: #e6e8ef;
  --border-soft: #eef0f5;
  --text: #10121a;
  /* Contrast: --muted is used on the tinted selected-chip background too, so
     it has to clear 4.5:1 against #e0e0f7, not just against white. */
  --muted: #565e70;
  /* #949cb0 measured 2.75:1 on white. --faint labels real data (column
     headers, dimensions), so it has to clear 4.5:1 like any other text. */
  --faint: #6f7688;
  --accent: #4f46e5;
  --accent-2: #0ea5e9;
  --accent-ink: #fff;
  --ok: #047857;
  --warn: #a35c05;
  --ad-bg: #f2f3f7;
  --ad-border: #dfe2ea;
  --ad-ink: #565d6c;
  --shadow: 0 10px 34px -14px rgba(16,18,26,.22);
  --shadow-sm: 0 1px 3px rgba(16,18,26,.08);
}

* { box-sizing: border-box; }
/* min-height, not height: with `height: 100%` the html element is exactly the
   viewport tall, its overflow does not propagate, and body becomes a scroll
   container that nothing can drive — so content below the fold is unreachable
   on any device. Symptom: window.scrollTo does nothing and scrollY stays 0. */
html, body { min-height: 100%; }
body {
  margin: 0;
  font-family: var(--f-sans);
  color: var(--text);
  background: var(--bg);
  background-image: var(--bg-grad);
  background-attachment: fixed;
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
button, input, select { font: inherit; color: inherit; }

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

/* Keyboard focus has to be visible. There was no focus style at all before,
   which is only survivable while almost nothing was focusable. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-sm); }
/* The file input is clipped, so surface its focus on the dropzone instead. */
.drop:has(.drop__input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── app shell ───────────────────────────────────────────────────────── */
.app { min-height: 100%; display: flex; flex-direction: column; }
.app__body { flex: 1; width: 100%; }

/* ── ad slot: fixed box, never collapses the layout ──────────────────── */
.ad {
  position: relative;
  display: grid; place-items: center;
  background: var(--ad-bg);
  border: 1px dashed var(--ad-border);
  border-radius: var(--r-md);
  color: var(--ad-ink);
  font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  overflow: hidden;
}
.ad::before {
  content: "Advertisement";
  position: absolute; top: 6px; left: 8px;
  font-size: 8px; letter-spacing: .1em; opacity: .6;
}
.ad--mpu        { height: 250px; width: 100%; }
.ad--banner-sm  { height: 50px; width: 100%; }
.ad--banner-top { height: 50px; width: 100%; max-width: 320px; margin-inline: auto; }
.ad--skyscraper-l, .ad--skyscraper-r { height: 600px; width: 300px; }
.ad--inline     { height: 120px; width: 100%; }
/* No opacity here: fading the hint toward the slot background dropped it to
   1.93:1. The uppercase "ADVERTISEMENT" label already reads as chrome. */
.ad__hint { font-size: 11px; letter-spacing: 0; text-transform: none; }
/* A live slot fills with the sandboxed frame; the hint gives way to it, while
   the "Advertisement" label stays on top so the placement is never unlabelled.
   An unfilled slot brings the hint back, so a refused fill reads as the
   reserved placeholder rather than an empty box. */
.ad--live[data-ad-state="filled"] .ad__hint { display: none; }
.ad--live::before { z-index: 2; }
.ad__frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: transparent; }
html[data-ads="off"] .ad { display: none; }
.ad-slot-mpu { min-height: 250px; }
.ad-slot-banner-sm { min-height: 50px; }

/* ── top bar ─────────────────────────────────────────────────────────── */
.topbar {
  display: flex; align-items: center; gap: 14px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--border-soft);
}
.brand { display: flex; align-items: center; gap: 9px; font-weight: 700; letter-spacing: -.01em; }
.brand__mark {
  width: 26px; height: 26px; border-radius: 8px;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  display: grid; place-items: center; color: var(--accent-ink); font-size: 13px;
}
.brand__sub { color: var(--faint); font-weight: 500; font-size: 12px; }

/* ── tabs ────────────────────────────────────────────────────────────── */
.tabs { display: flex; gap: 4px; padding: 4px; background: var(--panel-2); border: 1px solid var(--border-soft); border-radius: var(--r-md); }
.tab {
  border: 0; background: transparent; color: var(--muted);
  padding: 6px 14px; border-radius: var(--r-sm); cursor: pointer; font-weight: 550;
}
.tab.is-on { background: var(--panel); color: var(--text); box-shadow: var(--shadow-sm); }

/* ── dropzone ────────────────────────────────────────────────────────── */
.drop {
  border: 1.5px dashed var(--border);
  border-radius: var(--r-lg);
  background: var(--panel);
  padding: 30px 24px;
  text-align: center;
  transition: border-color var(--dur), background var(--dur), transform var(--dur);
  cursor: pointer;
}
.drop:hover, .drop.is-over { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, var(--panel)); }
.drop.is-over { transform: scale(1.005); }
/* Clipped rather than display:none, so the input stays focusable and the
   label gives it keyboard activation for free. */
.drop__input {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
.drop__input:focus-visible { outline: none; }
.drop__label { display: block; cursor: pointer; }
.drop__title, .drop__sub { display: block; }
.drop__icon {
  width: 46px; height: 46px; margin: 0 auto 12px;
  border-radius: var(--r-md); display: grid; place-items: center;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent); font-size: 20px;
}
.drop__title { font-weight: 650; font-size: 15px; margin-bottom: 4px; }
.drop__sub { color: var(--muted); font-size: 12.5px; }
.drop__cta { color: var(--accent); font-weight: 600; }

/* ── buttons ─────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  border-radius: var(--r-md); padding: 9px 16px;
  border: 1px solid var(--border); background: var(--panel-2); color: var(--text);
  font-weight: 600; cursor: pointer;
  transition: filter var(--dur), transform var(--dur), border-color var(--dur);
}
.btn:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.btn:active { transform: translateY(1px); }
.btn--primary {
  background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent-2) 55%, var(--accent)));
  border-color: transparent; color: var(--accent-ink);
  box-shadow: 0 8px 22px -10px color-mix(in srgb, var(--accent) 70%, transparent);
}
.btn--primary:hover { filter: brightness(1.07); }
.btn--lg { padding: 12px 22px; font-size: 15px; border-radius: var(--r-lg); }
.btn--sm { padding: 5px 11px; font-size: 12px; border-radius: var(--r-sm); }
.btn--ghost { background: transparent; }
.btn[disabled] { opacity: .5; cursor: not-allowed; }

/* ── panels & layout ─────────────────────────────────────────────────── */
.panel { background: var(--panel); border: 1px solid var(--border-soft); border-radius: var(--r-lg); box-shadow: var(--shadow); }
.panel__head { padding: 13px 16px; border-bottom: 1px solid var(--border-soft); font-weight: 650; font-size: 13px; display: flex; align-items: center; gap: 8px; }
.panel__body { padding: 16px; }

.stack { display: grid; gap: 14px; }
.row { display: flex; gap: 10px; align-items: center; }
.row--wrap { flex-wrap: wrap; }
.spacer { flex: 1; }

/* ── fields ──────────────────────────────────────────────────────────── */
.field { display: grid; gap: 6px; }
.field__label { font-size: 11.5px; font-weight: 600; color: var(--muted); display: flex; justify-content: space-between; }
.field__val { color: var(--text); font-variant-numeric: tabular-nums; }
.select, .input {
  width: 100%; padding: 8px 11px; border-radius: var(--r-sm);
  background: var(--panel-2); border: 1px solid var(--border); color: var(--text);
}
/* The native select arrow is drawn against the element's own right edge, so it
   ignores padding and sits ~1px off the border — at every font size. `appearance:
   none` takes it away and an inline SVG chevron replaces it at a fixed inset, so
   the gap between the arrow and the border is a number rather than whatever the
   platform decided. The focus ring and the open state are unaffected because both
   were never on the native control to begin with. */
.select {
  appearance: none; -webkit-appearance: none;
  padding-right: 34px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%236b7280' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
}
input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 4px; border-radius: 99px;
  /* The track is painted up to the thumb. `--pct` is set from the value by
     `range()` in view.js and updated on input by app.js, because a CSS-only
     "filled track" is not expressible: the value lives in the DOM, not in CSS. */
  background: linear-gradient(to right, var(--accent) 0 var(--pct, 0%), var(--border) var(--pct, 0%) 100%);
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 16px; height: 16px; border-radius: 99px;
  background: var(--accent); border: 2px solid var(--panel); cursor: grab;
  box-shadow: 0 1px 5px rgba(0,0,0,.4);
}
/* Firefox draws the filled part itself and ignores the track gradient, so it gets
   its own rule rather than the flat `--border` the gradient would give it. */
input[type="range"]::-moz-range-progress { background: var(--accent); height: 4px; border-radius: 99px; }
input[type="range"]::-moz-range-track { background: var(--border); height: 4px; border-radius: 99px; }
input[type="range"]:disabled { opacity: .5; cursor: not-allowed; }

/* ── format chips ────────────────────────────────────────────────────── */
/* A fixed four columns, not `auto-fill`: the count of columns must not depend
   on the panel's width, or the number of rows the nine formats wrap to — and so
   the height of the whole controls panel — changes between breakpoints. The
   queue and the Convert button sit below this panel; a grid that reflows would
   move them when the window crossed a width, not only when the tab changed. */
.chips { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
/* Under 380px a four-across grid squeezes each chip to ~66px and its label wraps
   to a second line, which makes the grid taller again — the very reflow the
   fixed count was for. Three across keeps the label on one line. */
@media (max-width: 380px) { .chips { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.chip {
  padding: 9px 8px; border-radius: var(--r-md);
  border: 1px solid var(--border); background: var(--panel-2);
  cursor: pointer; text-align: center; transition: all var(--dur);
}
.chip:hover { border-color: var(--accent); }
.chip.is-on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 13%, var(--panel-2)); }
.chip__fmt { font-weight: 700; font-size: 13px; display: block; }
.chip__use { font-size: 10.5px; color: var(--muted); margin-top: 1px; display: block; }

/* ── unsupported-browser overlay ───────────────────────────────────── */
.overlay {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center; padding: 24px;
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(6px);
  overflow: auto;
}
.overlay__card {
  width: 100%; max-width: 460px;
  padding: 28px 26px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
  text-align: center;
}
.overlay__icon {
  width: 44px; height: 44px; margin: 0 auto 14px;
  display: grid; place-items: center; border-radius: 99px;
  background: color-mix(in srgb, var(--warn) 16%, transparent);
  color: var(--warn); font-size: 20px;
}
.overlay__title { font-size: 19px; font-weight: 700; margin: 0 0 8px; letter-spacing: -.01em; }
.overlay__detail { color: var(--muted); font-size: 13.5px; margin: 0 0 18px; }
.overlay__needs {
  text-align: left; font-size: 11.5px; font-weight: 650; color: var(--muted);
  text-transform: uppercase; letter-spacing: .07em; margin: 0 0 8px;
}
.overlay__list {
  text-align: left; margin: 0 0 18px; padding: 0 0 0 20px;
  color: var(--muted); font-size: 13px;
}
.overlay__list li { margin-bottom: 5px; }
.overlay__hint { color: var(--faint); font-size: 12.5px; margin: 0; }

/* A one-line note for things that are merely unavailable, as opposed to the
   overlay, which is for when nothing works. */
.note-line { color: var(--faint); font-size: 12px; margin: 2px 0 0; }

/* ── progress bar ──────────────────────────────────────────────────── */
.bar { height: 4px; border-radius: 99px; background: var(--border); overflow: hidden; margin-top: 5px; }
.bar__fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width .25s; }

.ring { width: 18px; height: 18px; }

/* ── stat strip ──────────────────────────────────────────────────────── */
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.stat { padding: 11px 13px; border-radius: var(--r-md); background: var(--panel-2); border: 1px solid var(--border-soft); }
.stat__k { font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .07em; }
.stat__v { font-size: 17px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }

/* ── bottom ad bar ────────────────────────────────────────────────────── */
/* Pinned to the foot of the viewport for the length of the page. `sticky` rather
   than `fixed` on purpose: the bar is the last child of a full-height flex column,
   so it reads as a fixed bottom bar, and being in flow means it never covers the
   last row of results. `position: fixed` would need a body padding-bottom equal
   to its own height, and that padding is then wrong the moment the bar is hidden. */
.sticky-ad {
  position: sticky; bottom: 0; z-index: 20;
  padding: 8px 20px 10px;
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  backdrop-filter: blur(10px);
  border-top: 1px solid var(--border-soft);
}
/* Each box is capped to the creative that actually fills it. A 728x90 unit in a
   320px column is a letterbox: the frame is centred with the slot's dashed
   background showing either side, which reads as a broken image rather than an
   ad. So the bar carries one box per breakpoint and only one is ever displayed. */
.sticky-ad .ad { margin: 0 auto; }
.ad--leaderboard-bottom { height: 90px; width: 728px; max-width: 100%; }
.ad--banner-sm           { height: 50px; width: 320px; max-width: 100%; }
/* The 728 needs 728px plus the bar's own 20px padding each side, so 820px is the
   first width it fits at its natural size. Below it the 320x50 serves instead.
   The defaults come first and the query overrides them: written the other way
   round, the trailing `display: none` wins at every width and the bar is empty on
   desktop, which is exactly what happened the first time. */
.sticky-ad .ad--banner-sm { display: block; }
.sticky-ad .ad--leaderboard-bottom { display: none; }
@media (min-width: 820px) {
  .sticky-ad .ad--banner-sm { display: none; }
  .sticky-ad .ad--leaderboard-bottom { display: block; }
}

/* ══ THEME B — One-Box ═══════════════════════════════════════════════ */

/* The rail is the horizontal ad frame: the content column, and the 300x600 slots
   either side of it. It is one column by default, because a 300px rail beside a
   375px phone leaves nothing for the converter. Each placement is *absent* at the
   widths where it does not fit rather than merely narrow, because a reserved box
   for a creative the reader cannot see is page furniture for nothing.

   The rails are centred vertically against the content column rather than pinned
   to its top. A 600px rail hung off the top of a column that can run to 1800px
   sits beside the dropzone and nowhere near the results, which is where the eye
   goes after converting; centred, it spans the middle of the task. The centring
   belongs to the rails alone (`align-self: center`), never to the column: see
   the 1180px block below. */
.rail {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 24px;
  justify-items: center;
  width: 100%;
}
.rail__side { display: none; }

/* 1180px is 300 + 640 + 300 plus the two 24px gutters and the page's own 20px
   padding: the first width where the content column still gets its 640px. */
@media (min-width: 1180px) {
  .rail {
    grid-template-columns: 300px minmax(0, 1fr) 300px;
    /* `start`, not `center`. Centring the *column* against the 600px rails made
       its top depend on its own height, so switching to the Video tab — which
       changes that height — moved the whole converter by the difference (37px
       at 1440). The rails are still centred, but on their own, so the column's
       top is fixed and a tab switch does not shift the page. */
    align-items: start;
  }
  .rail__side { display: block; align-self: center; }
}

/* `width: 100%` is load-bearing. Without it, `main` is a grid item of `.rail`,
   whose `justify-items: center` shrink-wraps it to its widest child's intrinsic
   width — at 820-1179px that came out 333px, a third of the screen, instead of
   the 640px the column is allowed. The cap keeps the single column readable on a
   wide screen; the 1180px block lifts it once the rails take over. */
.lay-b { display: grid; gap: 22px; padding: 26px 20px 40px; justify-items: center; width: 100%; max-width: 640px; }
.lay-b__hero { text-align: center; max-width: 560px; margin-inline: auto; }
.lay-b__title { font-size: 30px; font-weight: 750; letter-spacing: -.03em; margin: 0 0 8px; }
.lay-b__lead { color: var(--muted); font-size: 14.5px; }
.lay-b__card { width: 100%; max-width: 640px; }
.lay-b__zone { padding: 34px 24px; }
.lay-b__below { width: 100%; max-width: 640px; display: grid; gap: 14px; }

/* Width, in the opposite order from the rules it overrides: these come after the
   `.lay-b` box above, because same-specificity rules are decided by source order
   and a media query earlier in the file loses to one later on. Written above it,
   the column stayed centred and 560 wide and every measurement said so.

   The content column takes the width the rails leave it. Centred but not
   stretched, `main` shrink-wraps its widest child, so at 1440 it was 560px of a
   752px column and the leftover was gutter. */
@media (min-width: 1180px) {
  .lay-b { justify-items: stretch; justify-self: stretch; max-width: none; }
  .lay-b__card, .lay-b__below { max-width: none; }
}

/* 1400px is where the middle column is wide enough to split in two without
   making either half a column of two word-wrapped chips: the rail leaves
   1400-648=752, less the page's 40px padding and the 22px grid gap, so 345px
   each, 313 of it inside the panel padding — three chips across.

   Side by side, the dropzone and the controls both start at the top, so the
   Convert button lands around 570px instead of 800px: above the pinned bottom
   bar rather than behind it. The queue spans both columns underneath, because a
   filename truncated to "sam…" in a half column is not a filename.

   The `.lay-b__card`/`.lay-b__below` overrides above must stay before this one:
   at 1400px the card keeps its `width: 100%` and `max-width: none`, which is
   what makes column one fill. */
@media (min-width: 1400px) {
  .lay-b { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
  #region-hero, .lay-b__topad { grid-column: 1 / -1; }
  .lay-b__card { grid-column: 1; }
  /* The wrapper stops being a box, so its two children become grid items of
     `.lay-b` itself: the settings panel takes the second column beside the
     dropzone, the queue takes the row underneath, full width. */
  .lay-b__below { display: contents; }
  #region-settings { grid-column: 2; }
  #region-queue { grid-column: 1 / -1; }
  /* Prerendered: `#region-static` carries the landing hero and a working drop
     target until the module boots. It is not part of the live layout, and
     auto-placed it would take the first half column — a landing page whose
     heading and dropzone sat in the left half with an empty column beside them.
     It spans the row and keeps its own centred 640px column, as before. */
  #region-static { grid-column: 1 / -1; }
  #region-static .lay-b__card { max-width: 640px; margin-inline: auto; }
}

/* Switching between the Images and Video tabs must not move the page. The
   controls panel is the tallest item in its row, and the Video tab's panel is
   26px shorter than the Images tab's — selects where the other has a chip grid,
   a Convert row the other does not. The queue and the download row sit below
   this panel, so the shorter panel lifted them. Reserving the Images tab's
   height (measured with the format grid at four fixed columns, so it is the same
   at every width) keeps everything under the panel where the reader left it. */
#region-settings { min-height: 398px; }

/* The dropzone says two different things, and the stylesheet decides which.
   "Drop images here" and "paste with ⌘V" are instructions a phone cannot
   follow: there is no drag to drop from and no command key. A coarse pointer
   with no hover gets the wording that matches what it can actually do.

   `display: none` rather than `visibility`, so the wrong wording is also gone
   from the accessibility tree instead of being read aloud after the right one. */
.drop__coarse { display: none; }
@media (hover: none) and (pointer: coarse) {
  .drop__fine { display: none; }
  .drop__coarse { display: inline; }
}
/* The mobile top banner only exists while the rails are gone. Above 1180px the
   rail already frames the content, and a banner there would be a fourth slot in
   one screen for no reach the rail has not got. */
.lay-b__topad { display: block; width: 100%; }
@media (min-width: 1180px) { .lay-b__topad { display: none; } }

/* ── queue table ─────────────────────────────────────────────────────
   Header and rows must share one grid. They once did not, and the header
   rendered "SIZE INSIZE OUTΔ" because it was laid out differently from the
   rows. .qhead carries .tbl, so .tbl's display:grid wins by source order. */
.qhead {
  display: flex; align-items: center;
  padding: 0 12px 2px;
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .07em; color: var(--faint);
}
.tbl { display: grid; grid-template-columns: 1fr 92px 108px 74px; gap: 10px; align-items: center; padding: 0 12px; }
.tbl > span:nth-child(n+2) { text-align: right; }
.tbl__cell { font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }

/* video notice */
.notice {
  display: flex; gap: 9px; padding: 10px 12px; border-radius: var(--r-md);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
  color: var(--text); font-size: 12px;
}
.notice b { display: block; margin-bottom: 2px; }
.notice span { color: var(--muted); }

/* A landing page whose promised output this browser cannot write. Sits inside
   the hero, so it is a line under the heading rather than a banner. */
.notice-line {
  display: flex; gap: 8px; margin: 10px 0 0; padding: 9px 11px;
  border-radius: var(--r-md); font-size: 12px; text-align: left;
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 30%, transparent);
}

.footer {
  padding: 20px; text-align: center; color: var(--faint); font-size: 12px;
  border-top: 1px solid var(--border-soft);
}
.footer a { color: var(--muted); }

/* The pricing panel, the licence input, the chip lock glyph and the `.gate`
   refusal notice all went with the Pro tier. Each had exactly one consumer, so
   there was nothing left to keep. */
.row .input { flex: 1; min-width: 0; font-family: var(--f-mono); letter-spacing: -.01em; }

.empty { color: var(--faint); font-size: 12.5px; text-align: center; padding: 20px 0; }

/* ── landing page copy ───────────────────────────────────────────────────
   The prose under a prerendered pair page. It lives outside `#root` because
   `mount()` replaces that whole subtree, so prerendered copy placed inside it
   would vanish the moment the app boots. */
.seo { padding: 0 20px 48px; display: flex; justify-content: center; }
.seo__col { width: 100%; max-width: 640px; }
.seo h2 { font-size: 19px; font-weight: 700; letter-spacing: -.01em; margin: 0 0 14px; }
.seo h3 { font-size: 16px; font-weight: 650; margin: 26px 0 8px; }
.seo h4 { font-size: 14.5px; font-weight: 650; margin: 16px 0 5px; }
.seo p { color: var(--muted); font-size: 14.5px; margin: 0 0 12px; }
.seo ul { margin: 0 0 12px; padding-left: 20px; }
.seo li { color: var(--muted); font-size: 14.5px; margin-bottom: 6px; }
.seo a { color: var(--accent); }

/* ── responsive ──────────────────────────────────────────────────────── */
@media (max-width: 720px) {
  html { overflow-x: hidden; }
  body { overflow-x: hidden; }
  .brand__sub { display: none; }
  .topbar { padding: 10px 14px; gap: 10px; }
  .lay-b__title { font-size: 25px; }
  .sticky-ad { padding: 8px 12px 10px; }
  /* Give the filename real room; the numeric columns can afford to be tighter
     than at desktop width, and a name truncated to "sam…" is not a filename. */
  .tbl { grid-template-columns: minmax(0, 1.6fr) 62px 70px 50px; gap: 6px; }
  .tabs { width: 100%; }
  .tab { flex: 1; }
  .btn--lg { width: 100%; }
  .row--wrap > .btn { flex: 1; }
  /* a 300x250 unit is the whole screen in one column */
  .ad--mpu { max-width: 320px; margin-inline: auto; }
}