/* ══════════════════════════════════════════════════════════════════════
   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);

  /* Spacing, on a 4px base. The older rules above and below still spell their
     sizes out; new rules use these, and an old rule moves over when it is
     next touched. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-8: 32px;
  --sp-10: 40px;
  --sp-12: 48px;

  /* Long-form text: the guide and FAQ under the converter, the copy under each
     pair page, and the footer. Prose grows to 16px on phones (below). */
  --fs-prose: 14.5px;
  --fs-h2: 19px;
  --fs-h3: 16px;
  --fs-h4: 14.5px;
  --fs-small: 12px;
  --lh-prose: 1.6;

  /* Smallest tap target: 24px for a mouse (WCAG 2.2), 44px for a finger. */
  --tap-min: 24px;
}
@media (pointer: coarse) {
  :root { --tap-min: 44px; }
}
@media (max-width: 720px) {
  :root { --fs-prose: 16px; }
}

* { 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 {
  /* `dvh` alongside the `%` above: `100%` of `html` never resolves, so the body
     was only as tall as its content and had no leftover height for the shell to
     grow into. A column, so the shell is the one child that grows. */
  display: flex; flex-direction: column; min-height: 100dvh;
  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 ───────────────────────────────────────────────────────── */
/* The body and `#root` are columns at least a viewport tall, and the shell is
   the one child that grows into the leftover height. The sticky foot bar is the
   last child of the shell and is meant to sit at the foot of the *viewport*;
   while the shell was only as tall as its content the bar sat at the end of the
   content instead, so the landing page showed a band of blank page below the
   bar — which reads as the ad's own container taking up room.
   `flex: 1 0 auto`, not `min-height`: a page long enough to scroll (a pair page,
   whose copy sits after `#root`) has no leftover height, so the shell keeps its
   natural height and the copy stays above the fold. */
#root { flex: 1 0 auto; display: flex; flex-direction: column; }
.app { flex: 1 0 auto; 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; }
/* The frame holds the placement's box, and it is invisible until it holds a
   creative. An unfilled request leaves the network's empty unit in the frame, and
   an empty unit still paints: a grey rectangle where the rail's placeholder or
   the pinned bar's label should be, which reads as a broken ad. `opacity` rather
   than `display`, because the frame's own viewport is what the network measures —
   a frame that is not laid out is a frame that cannot fill. Only `filled` shows
   it; pending, unfilled and blocked all leave the page's own chrome on view. */
.ad__frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: transparent; opacity: 0; }
.ad--live[data-ad-state="filled"] .ad__frame { opacity: 1; }
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; }

/* Back: bordered, 44 px, first in the top bar on every page. */
.back {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 44px; min-width: 44px; padding: 0 14px 0 10px; flex: none;
  border-radius: var(--r-md); border: 1px solid var(--border);
  background: var(--panel-2); color: var(--text);
  font-weight: 600; text-decoration: none; white-space: nowrap;
}
.back:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.back svg { width: 20px; height: 20px; flex: none; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }

/* ── 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.

   The bar carries no chrome of its own. Its padding, tinted strip, blur and
   hairline were four ways of spending vertical space around a box that is empty
   until the network answers, and the tint read as a grey band at the foot of the
   page. What a reader sees here is the creative, or its label, and nothing else. */
.sticky-ad { position: sticky; bottom: 0; z-index: 20; }
/* The reserved size belongs to the creative, not to the placeholder. Until one
   lands the slot is the height of its label: an unfilled 728x90 used to sit at
   the foot of every page as 90px of grey box. The fill and the dashed outline go
   with it — an empty box is not a placement. */
.sticky-ad .ad {
  margin: 0 auto;
  width: auto; height: auto; max-width: 100%;
  padding: 4px 0;
  background: none; border: 0; border-radius: 0;
  /* The slot is `display: block` here (the breakpoint rules below own show/hide),
     so the label is an inline box and this centres it; `place-items` from `.ad`
     has nothing to lay out. */
  text-align: center;
}
/* The label, not a hint. A hint names the box a placement reserves, and this one
   reserves nothing until a creative arrives; the bar's `aria-label` already names
   the placement, so the chrome label is all the compact slot carries. */
.sticky-ad .ad::before { position: static; }
/* A creative needs the box it was sold as, and only a creative gets one:
   `data-ad-state="filled"` is written on the slot when the frame reports a fill,
   so pending, unfilled and blocked all stay the height of the label. A 728x90
   unit in a 320px column is a letterbox — the frame centred with the slot's
   background showing either side, which reads as a broken image — so the bar
   carries one box per breakpoint and only one is ever displayed. */
.sticky-ad .ad[data-ad-state="filled"] { padding: 0; text-align: start; }
.sticky-ad .ad[data-ad-state="filled"]::before { position: absolute; top: 6px; left: 8px; }
.sticky-ad .ad--leaderboard-bottom[data-ad-state="filled"] { height: 90px; width: 728px; max-width: 100%; }
.sticky-ad .ad--banner-sm[data-ad-state="filled"]           { height: 50px; width: 320px; max-width: 100%; }
/* The frame keeps the size the placement was sold at, even while the bar is only
   a label tall and clipping it. The network measures its own viewport, so a frame
   squeezed to the label's height would be asking for a 728x90 unit inside 25px of
   room. It is invisible until it has a creative (the rule on `.ad__frame`), so
   what shows meanwhile is the label. */
.sticky-ad .ad__frame { right: auto; bottom: auto; top: 0; left: 50%; transform: translateX(-50%); }
.sticky-ad .ad--leaderboard-bottom .ad__frame { width: 728px; height: 90px; }
.sticky-ad .ad--banner-sm .ad__frame           { width: 320px; height: 50px; }
/* 820px is the first width a 728 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; }
}

/* An ads-off build ships no rail <aside>s at all. The three-column grid would
   still reserve 300px a side and place the converter in the middle track, so
   the track list collapses with the markup it framed. */
@media (min-width: 1180px) {
  .rail--noads { grid-template-columns: minmax(0, 1fr); }
}

/* `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);
}

/* 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 var(--sp-5) var(--sp-12); display: flex; justify-content: center; line-height: var(--lh-prose); }
.seo__col { width: 100%; max-width: 640px; }
.seo h2 { font-size: var(--fs-h2); font-weight: 700; letter-spacing: -.01em; line-height: 1.3; margin: 0 0 var(--sp-3); }
.seo h3 { font-size: var(--fs-h3); font-weight: 650; line-height: 1.3; margin: var(--sp-6) 0 var(--sp-2); }
.seo h4 { font-size: var(--fs-h4); font-weight: 650; margin: var(--sp-4) 0 var(--sp-1); }
.seo p { color: var(--muted); font-size: var(--fs-prose); margin: 0 0 var(--sp-3); }
.seo ul, .seo ol { margin: 0 0 var(--sp-3); padding-left: var(--sp-5); }
.seo li { color: var(--muted); font-size: var(--fs-prose); margin-bottom: var(--sp-2); }
.seo strong { color: var(--text); font-weight: 650; }
.seo a { color: var(--accent); text-underline-offset: 2px; }
.seo kbd {
  font-family: var(--f-mono); font-size: .88em;
  padding: 0 var(--sp-1); border: 1px solid var(--border); border-bottom-width: 2px;
  border-radius: var(--r-sm); background: var(--panel); color: var(--text);
}

/* ── home guide ──────────────────────────────────────────────────────────
   The home page's how-to, format guide and FAQ, under the converter. It is
   static HTML in `index.html`, outside `#root`, so a reader that runs no
   script gets all of it. The page is tool-first: the converter keeps the
   first screen and this starts below it. */
.seo--guide { padding-top: var(--sp-8); }
.seo--guide h2:not(:first-child) { margin-top: var(--sp-10); }

/* Steps: the number carries the order, the bold lead names the step. */
.seo__steps li { padding-left: var(--sp-1); }
.seo__steps li::marker { color: var(--faint); font-weight: 650; }

/* Format guide: the name and its chip label on one line, the advice under it,
   the same pairing the chips use in the converter. */
.seo__formats { margin: 0 0 var(--sp-3); }
.seo__formats dt {
  font-size: var(--fs-prose); font-weight: 650; color: var(--text);
  margin-top: var(--sp-4);
}
.seo__formats dt:first-child { margin-top: 0; }
.seo__formats dt span { color: var(--faint); font-weight: 500; margin-left: var(--sp-1); }
.seo__formats dd { margin: var(--sp-1) 0 0; color: var(--muted); font-size: var(--fs-prose); }

/* FAQ: native <details>, so it opens by keyboard and by tap with no script,
   and the answers are in the HTML whether or not a reader opens them. The
   question is a real <h3> inside the summary, so it stays in the heading
   outline. Hairlines rather than cards: one list, not ten boxes. */
.seo__faq { border-top: 1px solid var(--border); }
.seo__faq details { border-bottom: 1px solid var(--border); }
.seo__faq summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  min-height: var(--tap-min); padding: var(--sp-3) 0;
  cursor: pointer; list-style: none;
}
.seo__faq summary::-webkit-details-marker { display: none; }
.seo__faq summary h3 { margin: 0; font-size: var(--fs-prose); }
/* A chevron drawn from two borders, so it needs no icon font or asset. It
   points down while closed and up while open. */
.seo__faq summary::after {
  content: ""; flex: none;
  width: var(--sp-2); height: var(--sp-2); margin-right: var(--sp-1);
  border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--dur) ease-out;
}
.seo__faq details[open] > summary::after { transform: translateY(2px) rotate(-135deg); }
@media (hover: hover) and (pointer: fine) {
  .seo__faq summary:hover h3 { color: var(--accent); }
}
.seo__faq details > p { margin: 0 0 var(--sp-4); }
@media (prefers-reduced-motion: reduce) {
  .seo__faq summary::after { transition: none; }
}

/* ── site footer ─────────────────────────────────────────────────────────
   The studio's name and its policy pages, on every page (`src/seo/footer.ts`).
   The links are a row while they fit on one line and an ordered column when
   they would not: a container query on the footer's own width rather than a
   viewport breakpoint, because it is the row that wraps, not the screen.
   The query measures the footer's content box; the five links need 376px in a
   row (measured in Chromium at 1440px), so below 384px they stack. */
.site-footer {
  container-type: inline-size;
  margin-top: auto;
  padding: var(--sp-6) var(--sp-5);
  border-top: 1px solid var(--border-soft);
  text-align: center;
  font-size: var(--fs-small);
  color: var(--muted);
}
.site-footer__links {
  display: flex; justify-content: center; gap: var(--sp-1) var(--sp-3);
  margin: 0; padding: 0; list-style: none;
}
.site-footer__links a {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap-min); padding: 0 var(--sp-2);
  color: var(--muted); text-decoration: underline; text-underline-offset: 2px;
}
@media (hover: hover) and (pointer: fine) {
  .site-footer__links a:hover { color: var(--text); }
}
.site-footer__copy { margin: var(--sp-2) 0 0; color: var(--faint); }
@container (max-width: 384px) {
  .site-footer__links { flex-direction: column; gap: 0; }
  .site-footer__links a { display: flex; width: 100%; }
}

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