/* ============================================================
   anyjxl — Neubrutalist Design System
   ============================================================ */

/* --- Reset & Base --- */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* The UA stylesheet's `[hidden] { display: none }` is the weakest rule in the
   cascade, so any author rule that sets `display` silently defeats it. Every
   row in the settings pane is `display: flex`, which meant `el.hidden = true`
   set the property and changed nothing on screen: the Butteraugli distance
   field stayed visible in quality mode, showing whatever number it last held
   beside a slider that had already moved somewhere else. Restated here so
   hiding an element is a fact about the element, not a fact about whether its
   class happens to set display. */
[hidden] {
  display: none !important;
}

/* Adopted from the sibling, which had it and this app did not, so every label
   here that a sighted reader gets from position or an icon had to be repeated
   in an aria-label instead — a second copy of the words, translated by a
   different mechanism than the visible ones. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

:root {
  --black: #1a1a1a;
  --white: #fafafa;
  --gray: #e5e5e5;
  /* These three were #888, #3aa8d8 and #ff6b35, which measure 3.5:1, 2.6:1 and
     2.7:1 against --white. WCAG AA wants 4.5:1 for text, so the default theme —
     light, which is what most visitors see — failed on its links, its warnings
     and every hint under a settings heading. The values below are the sibling's
     and are 5.7:1, 5.6:1 and 6.6:1. test/design-tokens.test.mjs measures them. */
  --gray-dark: #666;
  --accent: #62c6f2;
  --accent-dark: #006b99;
  --warn: #a62f00;
  /* The green end of the same oklch ramp hueForSavings() drives, so a guarantee
     that holds is the same colour as a file that got smaller. Darkened from
     L 0.65, which measured 2.9:1 on --white. */
  --ok: oklch(0.52 0.14 140);
  --border: 3px solid var(--black);
  --shadow: 4px 4px 0 var(--black);
  --shadow-sm: 2px 2px 0 var(--black);
  --radius: 0;
  --font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --mono: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
  --ease: cubic-bezier(0.25, 0.46, 0.45, 0.94);

  color-scheme: light dark;
}

html {
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: var(--font);
  background: var(--white);
  color: var(--black);
  line-height: 1.6;
  min-height: 100dvh;
}

a {
  color: var(--accent-dark);
  text-decoration-thickness: 2px;
  text-underline-offset: 2px;
}
.browse-link {
  color: var(--accent-dark);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 2px;
}
a:hover {
  color: var(--black);
}

code {
  font-family: var(--mono);
  background: var(--gray);
  padding: 0.1em 0.3em;
  border: 1px solid var(--black);
  font-size: 0.9em;
}

strong {
  font-weight: 700;
}

/* --- Skip Link --- */
.skip-link {
  position: absolute;
  top: -100%;
  left: 0;
  z-index: 9999;
  padding: 0.5rem 1rem;
  background: var(--accent);
  color: var(--black);
  border: var(--border);
  font-weight: 700;
  text-decoration: none;
}
.skip-link:focus {
  top: 0;
}

/* --- Layout --- */
.container {
  max-width: 720px;
  margin: 0 auto;
  padding: 2rem 1rem;
}

/* --- Header --- */
header {
  position: relative;
  text-align: left;
  margin-bottom: 2rem;
  padding-top: 0.15rem;
}

header h1 {
  font-size: 3rem;
  font-weight: 900;
  letter-spacing: 0;
  line-height: 1;
  margin-bottom: 0.5rem;
  text-decoration: none;
  text-decoration-thickness: 0.16em;
  text-underline-offset: 0.08em;
  display: inline-flex;
  align-items: center;
  gap: 0.16em;
}
.brand-arrow {
  color: var(--accent-dark);
  font-size: 0.9em;
  line-height: 1;
  transform: translateY(-0.02em);
}

.tagline {
  font-size: 1rem;
  color: var(--gray-dark);
  max-width: 40ch;
  margin: 0;
}
.sibling-link {
  margin: 1.25rem 0 0;
  font-size: 0.85rem;
  color: var(--gray-dark);
  text-align: center;
}
.sibling-link a {
  text-decoration: none;
}
.sibling-link a:hover,
.sibling-link a:focus-visible {
  text-decoration-line: underline;
}

/* --- Language links (crawlable counterpart to the header picker) --- */
.lang-links {
  margin: 2.5rem 0 0;
  border-top: var(--border);
  padding-top: 1.25rem;
}
.lang-links-title {
  font-size: 0.8rem;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0 0 0.75rem;
  color: var(--gray-dark);
}
.lang-links-list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.lang-links-list a {
  display: block;
  padding: 0.3rem 0.6rem;
  border: 2px solid var(--black);
  background: var(--white);
  color: var(--black);
  font-size: 0.85rem;
  font-weight: 700;
  text-decoration: none;
  box-shadow: var(--shadow-sm);
}
.lang-links-list a:hover,
.lang-links-list a:focus-visible {
  background: var(--gray);
}
/* The accent stays the same light blue in dark mode, so its ink is pinned
   rather than inherited from --black, which inverts to near-white there. */
.lang-links-list a[aria-current='page'] {
  background: var(--accent);
  color: #1a1a1a;
}

.license-shell {
  width: min(72rem, calc(100% - 2rem));
  margin: 2rem auto 4rem;
}
.license-header {
  margin-bottom: 1.5rem;
}
.license-header h1 {
  font-size: clamp(2rem, 6vw, 3.25rem);
  margin: 0.5rem 0;
}
.license-section {
  margin-bottom: 1.5rem;
}
.license-section h2 {
  margin-bottom: 0.75rem;
}
.license-text {
  max-height: 28rem;
  overflow: auto;
  padding: 1rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  border: 2px solid var(--black);
  background: var(--white);
  font: 0.82rem/1.5 var(--mono);
}
.license-inventory {
  display: grid;
  gap: 0.75rem;
  margin-top: 1rem;
}
.license-inventory details {
  border: 2px solid var(--black);
  padding: 0.75rem 1rem;
  background: var(--white);
}
.license-inventory summary {
  cursor: pointer;
  font-weight: 800;
}
.license-inventory p,
.license-inventory ul {
  margin-top: 0.75rem;
}
.license-inventory ul {
  columns: 2;
  padding-left: 1.25rem;
}

/* --- Cards --- */
.card {
  border: var(--border);
  box-shadow: var(--shadow);
  padding: 1.25rem;
  margin-bottom: 1.5rem;
  background: var(--white);
}

.card h2,
.card h3 {
  font-size: 1.1rem;
  font-weight: 800;
  margin-bottom: 0.5rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.card p {
  margin-bottom: 0.5rem;
}
.card p:last-child {
  margin-bottom: 0;
}

.card--warning {
  border-color: var(--warn);
  background: #fff3ee;
}
.card--warning h2 {
  color: var(--warn);
}

.card--action {
  cursor: pointer;
  transition:
    transform 80ms var(--ease),
    box-shadow 80ms var(--ease);
}
.card--action:hover,
.card--action:focus-visible {
  transform: translate(-2px, -2px);
  box-shadow: 6px 6px 0 var(--black);
}

/* --- Drop Zone --- */
#drop-zone {
  border: 4px dashed var(--black);
  padding: 3rem 1.5rem;
  text-align: center;
  cursor: pointer;
  margin-bottom: 1.5rem;
  transition:
    background 120ms var(--ease),
    border-color 120ms var(--ease);
  background: var(--white);
}
#drop-zone:hover,
#drop-zone:focus-visible {
  background: var(--gray);
}
#drop-zone.drag-over {
  border-color: var(--accent);
  background: #e8f7fd;
  border-style: solid;
}
#drop-zone[aria-disabled='true'] {
  opacity: 0.4;
  pointer-events: none;
}

#drop-zone strong {
  font-size: 1.25rem;
  display: block;
  margin-bottom: 0.25rem;
}
.muted {
  color: var(--gray-dark);
  font-size: 0.875rem;
}

/* --- File Input (hidden) --- */
#file-input {
  display: none;
}

/* --- Download Group --- */
.download-group {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}
.download-group button {
  flex: 1;
  padding: 0.75rem 1rem;
  font-family: var(--font);
  font-size: 0.9rem;
  font-weight: 700;
  border: var(--border);
  background: var(--accent);
  color: var(--black);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition:
    transform 60ms var(--ease),
    box-shadow 60ms var(--ease);
}
.download-group button:hover:not(:disabled) {
  transform: translate(-1px, -1px);
  box-shadow: 3px 3px 0 var(--black);
}
.download-group button:active:not(:disabled) {
  transform: translate(1px, 1px);
  box-shadow: 1px 1px 0 var(--black);
}
.download-group button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
/* Secondary, like the sibling's: the accent belongs to Download, and a button
   that throws the results away must not look like the one that keeps them. */
.clear-results {
  background: var(--white) !important;
}
.auto-toggle {
  flex: 0 0 auto !important;
  background: var(--white) !important;
}
.auto-toggle[aria-pressed='true'] {
  background: var(--accent) !important;
}

/* --- File List --- */
.file-item {
  border: var(--border);
  padding: 0.75rem 1rem;
  margin-bottom: 0.75rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  background: var(--white);
}
.file-info {
  flex: 1;
  min-width: 0;
}
.file-name {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.file-progress {
  height: 4px;
  background: var(--gray);
  margin: 0.25rem 0;
  border: 1px solid var(--black);
}
.file-progress-bar {
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width 300ms var(--ease);
}
.file-progress-bar.complete {
  background: #22c55e;
}
.file-size {
  font-size: 0.8rem;
  color: var(--gray-dark);
}
.file-settings {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--gray-dark);
  margin-top: 0.125rem;
}
/* Transient, script-owned (showRowNotice) — what just happened, not what is. */
.file-notice {
  font-size: 0.78rem;
  font-weight: 600;
  margin-top: 0.25rem;
}
.file-notice--ok {
  color: var(--ok);
}
.file-notice--warn {
  color: var(--warn);
}
.size-reduction {
  font-weight: 700;
  color: oklch(0.65 0.2 var(--savings-hue, 100));
}
.warning-text {
  color: var(--warn);
  font-weight: 600;
}

/* The button on a refusal that named a remedy. 44px is the touch target
   measure-mobile-actions.mjs holds every action on a row to. Dark text on the
   accent rather than the other way round: this palette's accent is a light blue,
   and white on it is under 2:1. */
.quick-fix-btn {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: 0.35rem;
  padding: 0.35rem 0.75rem;
  border: 2px solid var(--black);
  background: var(--accent);
  color: var(--black);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  transition: background 80ms;
  touch-action: manipulation;
}
.quick-fix-btn:hover:not([disabled]) {
  background: var(--accent-dark);
}
.quick-fix-btn[disabled] {
  opacity: 0.4;
  cursor: progress;
}

.file-actions {
  display: flex;
  gap: 0.25rem;
  flex-shrink: 0;
}
.file-actions button {
  width: 2.75rem;
  height: 2.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--black);
  background: var(--white);
  cursor: pointer;
  font-size: 1rem;
  font-weight: 700;
  transition: background 80ms;
  touch-action: manipulation;
}
.file-actions button:hover:not([disabled]) {
  background: var(--gray);
}
.file-actions button[disabled] {
  opacity: 0.3;
  cursor: not-allowed;
}
/* Copy is offered only where the clipboard would actually take a JPEG XL, which
   web/clipboard-support.js decides once and publishes here. Hidden by default
   because the common answer is no: Chromium refuses the type outright, and a
   button whose only outcome is an apology should never have been drawn. It is
   `display: none` rather than `visibility` or `opacity` so it leaves the tab
   order and the accessibility tree with it. */
.file-actions .result-action-copy {
  display: none;
}
html[data-jxl-clipboard='yes'] .file-actions .result-action-copy {
  display: flex;
}
.meta-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.75rem;
  font-size: 0.8rem;
}

/* --- Language Selector --- */
.lang-selector {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 2;
  width: 4.7rem;
  height: 2rem;
}
.lang-picker-current {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.22rem;
  border: 2px solid var(--black);
  background: var(--white);
  color: var(--black);
  font-size: 0.82rem;
  font-weight: 900;
  line-height: 1;
  box-shadow: var(--shadow-sm);
  pointer-events: none;
}
.lang-picker-flag {
  font-size: 1rem;
}
.lang-picker-select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
.lang-selector:focus-within .lang-picker-current,
.lang-selector:hover .lang-picker-current {
  background: var(--gray);
}

/* --- Tabs --- */
.tabs {
  display: flex;
  gap: 0;
  margin-bottom: 0.75rem;
}
.tab {
  flex: 1;
  padding: 0.5rem;
  font-family: var(--font);
  font-size: 0.85rem;
  font-weight: 700;
  border: var(--border);
  background: var(--white);
  cursor: pointer;
  text-align: center;
}
.tab + .tab {
  border-left: 0;
}
.tab[aria-selected='true'] {
  background: var(--black);
  color: var(--white);
}

/* --- Savings --- */
.savings-results {
  font-size: 0.9rem;
}
.result-line {
  display: flex;
  justify-content: space-between;
  padding: 0.25rem 0;
  border-bottom: 1px solid var(--gray);
}
.result-line:last-child {
  border-bottom: 0;
}

/* --- Settings --- */
.settings-shell {
  margin: 1.5rem 0 2.25rem;
}
.settings-panel {
  max-width: none;
  margin: 0;
}
.settings-content {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.settings-group {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.settings-group > h2 {
  font-size: 1.1rem;
  font-weight: 900;
  margin: 0 0 0.75rem;
  text-transform: uppercase;
}
.settings-preamble {
  margin: -0.25rem 0 0.75rem;
  color: var(--gray-dark);
  font-size: 0.9rem;
}

.settings-block {
  background: var(--white);
  padding: 1.125rem;
}

/* --- Preset chips --- */
.preset-chips {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: 0.5rem;
}
.preset-chip {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.6rem 0.7rem;
  text-align: left;
  font-family: var(--font);
  border: 2px solid var(--black);
  background: var(--white);
  color: var(--black);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
}
.preset-chip:hover {
  background: var(--gray);
}
.preset-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.preset-chip.is-active {
  background: var(--accent);
  color: #1a1a1a;
}
.preset-chip__name {
  font-size: 0.85rem;
  font-weight: 800;
}
.preset-chip__detail {
  font-family: var(--mono);
  font-size: 0.7rem;
  opacity: 0.8;
}
.settings-block h3 {
  font-size: 1rem;
  text-transform: uppercase;
  margin-bottom: 0.25rem;
}
/* A card's title and its ?, on one line.
   Every explanatory paragraph on this panel is behind one of these buttons.
   Prose that shows and hides in place reflows the whole panel under the
   reader's cursor — the Lossy Transform card alone was five lines of text
   above three radios — and prose that is always there is a wall between the
   reader and the control they came for. The popover is neither: it opens over
   the page in the top layer, so nothing moves, and it closes with Esc. */
.block-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}
.block-head h3 {
  margin-bottom: 0;
}
.settings-block.is-disabled {
  opacity: 0.4;
  pointer-events: none;
}

/* Jumped-to settings, from the links in the format guides further down the page.
   The room is so the control does not land flush against the top of the
   viewport with its label scrolled out of sight, and the flash is so a jump into
   a long panel of similar-looking cards says which one it meant. Two seconds,
   once, and no layout shift: only the background moves. */
:target {
  animation: settings-jump 2s ease-out 1;
}
.settings-block:target,
.setting-row:target,
.radio-group > label:target {
  scroll-margin-block-start: 1.5rem;
}
@keyframes settings-jump {
  from {
    background-color: var(--accent);
  }
  to {
    background-color: transparent;
  }
}
@media (prefers-reduced-motion: reduce) {
  :target {
    animation: none;
  }
}

.disabled-reason-box {
  background: #fff3ee;
  border: 2px solid var(--warn);
  padding: 0.5rem;
  margin-bottom: 0.5rem;
  font-size: 0.8rem;
  font-weight: 600;
}

/* --- Form Controls --- */
.setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.375rem 0;
  gap: 0.75rem;
}
.setting-row label:first-child {
  font-weight: 600;
  font-size: 0.9rem;
}

.range-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
/* The control's own name, when the block holds more than one of them. Quality is
   written two ways and both are on screen, so each row has to say which one it
   is rather than leaving the reader to infer it from position. */
.range-row > label,
.range-row__name {
  font-size: 0.9rem;
  font-weight: 600;
  flex-shrink: 0;
}
/* A row whose name carries its own ? beside it. */
.range-row__name {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}
.range-row input[type='range'] {
  flex: 1;
}

/* Quality and Butteraugli distance are the same number in two units, so their
   rows share one grid: two sliders whose tracks began at different x positions
   would read as two unrelated controls, which is the confusion this card was
   rebuilt to remove. Grid rather than a min-width in ch on the label, because
   the column sizes itself to whichever name is longest in the language being
   read — "Distance" in English, "Расстояние" in Russian — instead of to a width
   guessed in one of them. */
#quality-controls {
  display: grid;
  grid-template-columns: max-content 1fr max-content;
  align-items: center;
  gap: 0.625rem 0.75rem;
  margin-top: 0.75rem;
}
/* The rows keep their own element so each is still something that can be found,
   styled and — should it ever need to be — hidden; `contents` is what puts their
   three children into the shared grid. `[hidden]` still wins over it: the
   restatement at the top of this file is `!important` for exactly this reason. */
#quality-controls > .range-row {
  display: contents;
}
/* The exact-value box is `type="text"`, not `type="number"`, because it accepts
   the spelling cjxl uses (`d1.0`) — so it misses the number-input rule below and
   used to render as a bare browser field. It sits permanently beside the quality
   row's own value now, and the two have to look like the same kind of thing. */
#distance {
  width: 5rem;
  padding: 0.375rem 0.5rem;
  font-family: var(--mono);
  font-size: 0.9rem;
  border: var(--border);
  background: var(--white);
  text-align: center;
}

/* Toggle Switch */
.toggle {
  position: relative;
  display: inline-block;
  width: 44px;
  height: 24px;
  flex-shrink: 0;
}
.toggle input {
  opacity: 0;
  width: 0;
  height: 0;
  position: absolute;
}
.toggle-track {
  position: absolute;
  inset: 0;
  cursor: pointer;
  background: var(--gray);
  border: 2px solid var(--black);
  transition: background 150ms;
}
.toggle-track::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  background: var(--black);
  transition: transform 150ms;
}
.toggle input:checked + .toggle-track {
  background: var(--accent);
}
.toggle input:checked + .toggle-track::after {
  transform: translateX(20px);
}
.toggle input:focus-visible + .toggle-track {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.toggle input:disabled + .toggle-track {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Names what an exclusive group is choosing between, which the group itself
   cannot say. Doubles as the group's accessible name via aria-labelledby. */
/* Names the radiogroup under it. Not a .hint: a caption is what a control is
   called, and every explanation on this panel is behind a ?. */
.choice-caption {
  margin: 0.75rem 0 0.5rem;
  font-size: 0.85rem;
  font-weight: 700;
}

/* Exclusive radio groups */
.radio-group {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
}
.radio-group label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.9rem;
  font-weight: 500;
  cursor: pointer;
}
/* An option and the ? that explains it. The button is a sibling of the label
   rather than a child, because a <button> inside a <label> is a control that
   selects the radio every time it is pressed. */
.radio-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.radio-row > label {
  flex: 1;
}
.radio-group input[type='radio'] {
  appearance: none;
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  border: 2px solid var(--black);
  border-radius: 50%;
  background: var(--white);
  cursor: pointer;
  position: relative;
}
.radio-group input[type='radio']:checked {
  background: var(--accent);
}
.radio-group input[type='radio']:checked::after {
  content: '';
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: var(--black);
}
.radio-group input[type='radio']:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Number inputs & stepper */
input[type='number'] {
  width: 5rem;
  padding: 0.375rem 0.5rem;
  font-family: var(--mono);
  font-size: 0.9rem;
  border: var(--border);
  background: var(--white);
  text-align: center;
}
input[type='number']::-webkit-inner-spin-button {
  -webkit-appearance: none;
}
input[type='number'] {
  -moz-appearance: textfield;
}

.stepper-row {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding-left: 1.75rem;
  margin: 0.25rem 0;
}
.stepper-row button {
  width: 2rem;
  height: 2rem;
  border: var(--border);
  background: var(--white);
  cursor: pointer;
  font-size: 1rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}
.stepper-row button:hover {
  background: var(--gray);
}
.stepper-row button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

input[type='range'] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 8px;
  background: var(--gray);
  border: 2px solid var(--black);
  outline: none;
}
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px;
  height: 20px;
  background: var(--accent);
  border: 2px solid var(--black);
  cursor: pointer;
}
input[type='range']::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border-radius: 0;
  background: var(--accent);
  border: 2px solid var(--black);
  cursor: pointer;
}

input[type='color'] {
  width: 44px;
  height: 32px;
  padding: 2px;
  border: var(--border);
  background: var(--white);
  cursor: pointer;
}

.metadata-sub {
  padding-left: 1rem;
  border-left: 3px solid var(--gray);
  margin-top: 0.5rem;
}

/* --- The ? on a settings row ---
   A declarative popover: `popovertarget` on the button, `popover` on the panel,
   no JavaScript anywhere in the path. Esc and click-outside come from the
   platform, and the top layer means the settings panel cannot clip it.

   Centred rather than anchored to its row: CSS anchor positioning is not yet
   everywhere the popover API is, and a 24rem card in the middle of the panel
   reads the same on a phone as on a desktop.

   There is deliberately no `display: none` on the closed state. An engine
   without the API ignores the attribute entirely and renders the help inline
   and permanently visible — a worse layout, and the failure mode this design
   was chosen for over a title= tooltip, which on touch shows nothing at all. */
.row-tools {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}
.explain-btn {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  padding: 0;
  font: 800 0.9rem/1 var(--font);
  color: var(--black);
  background: var(--white);
  border: 2px solid var(--black);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.explain-btn:hover {
  background: var(--accent);
  color: #1a1a1a;
}
.explain-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.explain {
  max-width: 24rem;
  max-height: 80dvh;
  overflow-y: auto;
  padding: 1rem;
  border: var(--border);
  box-shadow: var(--shadow);
  background: var(--white);
  color: var(--black);
}
.explain:popover-open {
  position: fixed;
  inset: auto;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
}
.explain::backdrop {
  background: rgb(0 0 0 / 0.45);
}
.explain h4 {
  font-size: 0.95rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin-bottom: 0.5rem;
}
.explain p {
  font-size: 0.9rem;
  margin-bottom: 0.75rem;
}
.explain-close {
  width: 100%;
  padding: 0.55rem;
  cursor: pointer;
  font: 800 0.85rem var(--font);
  text-transform: uppercase;
  color: #1a1a1a;
  background: var(--accent);
  border: 2px solid var(--black);
  box-shadow: var(--shadow-sm);
}
.explain-close:focus-visible {
  outline: 2px solid var(--black);
  outline-offset: 2px;
}

.subsection {
  padding-left: 1rem;
  border-left: 3px solid var(--gray);
  margin-top: 0.5rem;
}
.subsection.is-disabled {
  opacity: 0.4;
  pointer-events: none;
}

.hint {
  font-size: 0.8rem;
  color: var(--gray-dark);
  margin-bottom: 0.5rem;
}
.hint--warn {
  color: var(--warn);
}

hr {
  border: none;
  border-top: 2px solid var(--gray);
  margin: 0.75rem 0;
}

/* Reset button */
.reset-settings-button {
  width: 100%;
  padding: 0.75rem;
  font-family: var(--font);
  font-size: 0.9rem;
  font-weight: 700;
  border: var(--border);
  background: var(--warn);
  color: var(--white);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  margin-bottom: 1.5rem;
}
.reset-settings-button--bottom {
  padding: 1rem;
  font-size: 1rem;
  margin: 0 0 1.5rem;
}
.reset-settings-button:hover:not(:disabled) {
  transform: translate(-1px, -1px);
  box-shadow: 3px 3px 0 var(--black);
}
.reset-settings-button:disabled {
  opacity: 0.55;
  cursor: default;
  background: var(--gray-dark);
}

/* --- Compare Overlay --- */
.compare-overlay {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgba(0, 0, 0, 0.85);
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}
.compare-overlay.is-open {
  display: flex;
}
.compare-close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  z-index: 20;
  width: 44px;
  height: 44px;
  font-size: 1.5rem;
  border: var(--border);
  background: var(--white);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Clips the zoomed picture instead of letting it push the overlay around, and
   makes the stage the viewport the pan drag moves the image behind.
   `position: relative` is what the notice column anchors to: it has to sit over
   the picture but outside both panes, and the stage is the only box that is
   exactly the picture and belongs to neither side of the wipe. */
.compare-stage {
  position: relative;
  max-width: 100%;
  max-height: 80vh;
  overflow: hidden;
}
.compare-stage:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 4px;
}
.compare-stage.is-zoomed {
  cursor: grab;
}
.compare-stage.is-panning {
  cursor: grabbing;
}
/* At and past 100% the reader is looking at individual samples, so show them as
   samples. Smoothing here would invent detail the file does not contain, which
   is the one thing a pixel inspector must never do.
   Keyed to `is-zoomed`, this asked for nearest-neighbour on a *downscale*: a
   24 MP photograph fitted into an 800px stage is still an 8x reduction at zoom
   1.05, and point-sampling that is aliasing dressed up as fidelity. The class
   comes from compare-inspect.js, which resolves it against the rendered image
   box rather than against the element. */
.compare-stage.is-actual-size img,
.compare-stage.is-actual-size canvas {
  image-rendering: pixelated;
}

/* --- Compare inspection tools --- */
.compare-tools {
  position: absolute;
  top: 1rem;
  left: 1rem;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  max-width: min(26rem, calc(100% - 5rem));
}
.compare-tools__zoom {
  display: flex;
  gap: 0.15rem;
}
.compare-tool {
  min-width: 2.5rem;
  height: 2.25rem;
  padding: 0 0.6rem;
  font-family: var(--font);
  font-size: 0.8rem;
  font-weight: 800;
  border: 2px solid var(--black);
  background: var(--white);
  color: var(--black);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
}
.compare-tool:hover:not(:disabled) {
  background: var(--gray);
}
.compare-tool:disabled {
  opacity: 0.4;
  cursor: default;
}
.compare-tool[aria-pressed='true'] {
  background: var(--accent);
}
.compare-tool--zoomlevel {
  font-family: var(--mono);
  min-width: 3.75rem;
}
.compare-tools__help,
.compare-tools__blocked {
  flex-basis: 100%;
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 1.35;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}
.compare-tools__blocked {
  color: var(--warn);
}
.compare-readout {
  position: absolute;
  left: 1rem;
  bottom: 1rem;
  z-index: 2;
  padding: 0.5rem 0.65rem;
  border: 2px solid #fff;
  background: rgba(0, 0, 0, 0.78);
  font-family: var(--mono);
  font-size: 0.72rem;
  line-height: 1.4;
  color: #fff;
  white-space: pre;
}
.compare-readout[aria-hidden='true'] {
  display: none;
}
.compare-readout__stats {
  font-weight: 800;
  margin-bottom: 0.2rem;
}
.compare-readout__stats:empty {
  display: none;
}
.compare-legend {
  margin-top: 0.4rem;
}
.compare-legend.hidden {
  display: none;
}
/* The bar is a 256x1 image stretched: the ramp is the data, and letting the
   browser interpolate one row across the width is what makes it a gradient. */
.compare-legend__bar {
  display: block;
  width: 100%;
  height: 0.55rem;
  border: 1px solid rgba(255, 255, 255, 0.6);
  image-rendering: auto;
}
.compare-legend__scale {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  margin-top: 0.15rem;
  font-size: 0.64rem;
  opacity: 0.85;
}
.compare-difference {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.compare-loading {
  display: grid;
  place-items: center;
  gap: 0.9rem;
  min-width: min(18rem, 80vw);
  color: #fff;
  font-family: var(--font);
  font-weight: 800;
}
.compare-spinner {
  width: 3rem;
  height: 3rem;
  border: 4px solid rgba(255, 255, 255, 0.35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: compare-spin 0.8s linear infinite;
}
.compare-loading__text {
  font-size: 0.95rem;
  text-align: center;
}
@keyframes compare-spin {
  to {
    transform: rotate(360deg);
  }
}
.compare-content {
  position: relative;
  width: 100%;
  height: 100%;
}
/* The canvas is the animated-JXL player (jxl-animation.js); it has to lay out
   exactly like the <img> in the other pane or the two halves would not align. */
.compare-content img,
.compare-content canvas {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.compare-bar {
  position: absolute;
  bottom: 0;
  padding: 0.5rem 0.75rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 700;
  color: #fff;
  white-space: nowrap;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
.compare-bar--left {
  left: 0;
  background: rgba(255, 100, 50, 0.85);
}
.compare-bar--right {
  right: 0;
  background: rgba(30, 144, 255, 0.85);
}
.compare-bar__name {
  font-weight: 800;
}
.compare-bar__size {
  opacity: 0.9;
}
/* Notices stack down the top-right corner instead of each claiming it, and the
   corner is the *converted* pane's, not the stage's: this is a child of
   .compare-content in slot="second", so .pane-first paints over it exactly
   where the original is showing. That clipping is the feature — see
   buildNoticeStack in compare-overlay.js — and it is why there is no z-index
   here to lift the column out of its pane the way the stage-parented version
   needed one (panes at 1 and 2, divider at 10, handle at 11). Within the pane
   it follows the image in document order, which is all it takes to sit above
   it. Nothing here takes pointer events, so the wipe underneath still works. */
.compare-notices {
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.3rem;
  max-width: min(17rem, calc(100% - 1rem));
  pointer-events: none;
}
.compare-preview-notice {
  padding: 0.45rem 0.6rem;
  border: 2px solid #fff;
  background: rgba(0, 0, 0, 0.72);
  color: #fff;
  font-family: var(--font);
  font-size: 0.78rem;
  font-weight: 900;
  line-height: 1.15;
  text-transform: uppercase;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85);
}
.compare-preview-notice__detail {
  margin-top: 0.2rem;
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 1.2;
  text-transform: none;
}
/* The two lossless-transcode guarantees, colour-separated: one says the original
   file comes back, the other says only the picture does. */
.compare-preview-notice--exact {
  border-color: var(--ok);
}
.compare-preview-notice--coefficients {
  border-color: var(--warn);
}

/* --- Box inspector drawer --- */
.inspect-overlay {
  position: fixed;
  inset: 0;
  z-index: 400;
  background: rgba(0, 0, 0, 0.6);
  display: none;
  align-items: stretch;
  justify-content: flex-end;
}
.inspect-overlay.is-open {
  display: flex;
}
.inspect-panel {
  width: min(46rem, 100%);
  max-height: 100dvh;
  overflow: auto;
  background: var(--white);
  color: var(--black);
  border-left: var(--border);
  padding: 1.25rem;
}
.inspect-head {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  gap: 0.25rem 0.75rem;
  margin-bottom: 1rem;
}
.inspect-head h2 {
  font-size: 1.1rem;
  font-weight: 900;
  text-transform: uppercase;
}
.inspect-close {
  grid-row: 1 / span 2;
  width: 44px;
  height: 44px;
  font-size: 1.5rem;
  border: var(--border);
  background: var(--white);
  color: var(--black);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.inspect-close:hover {
  background: var(--gray);
}
.inspect-subject {
  grid-column: 1;
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--gray-dark);
  overflow-wrap: anywhere;
}
.inspect-section {
  border: 2px solid var(--black);
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
}
.inspect-section__title {
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.inspect-note {
  margin-top: 0.6rem;
  font-size: 0.82rem;
  color: var(--gray-dark);
}
.inspect-note--warn {
  color: var(--warn);
  font-weight: 600;
}
.inspect-roles {
  list-style: none;
  display: grid;
  gap: 0.25rem;
  margin: 0.75rem 0;
}
.inspect-role {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 0.75rem;
  font-size: 0.82rem;
  padding: 0.2rem 0;
  border-bottom: 1px solid var(--gray);
}
.inspect-role__name {
  font-weight: 700;
}
.inspect-role__size,
.inspect-role__share {
  font-family: var(--mono);
  text-align: right;
}
.inspect-role__share {
  color: var(--gray-dark);
  min-width: 4ch;
}
.inspect-role--codestream .inspect-role__name {
  color: var(--accent-dark);
}
.inspect-role--reconstruction .inspect-role__name {
  color: var(--ok);
}
.inspect-role--other .inspect-role__name {
  color: var(--warn);
}
.inspect-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 0.5rem;
  font-size: 0.78rem;
}
.inspect-table th,
.inspect-table td {
  padding: 0.3rem 0.4rem;
  border-bottom: 1px solid var(--gray);
}
.inspect-table th {
  text-align: left;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--gray-dark);
}
.inspect-table__h--offset,
.inspect-table__h--size,
.inspect-table__h--share {
  text-align: right;
}
.inspect-table__num {
  font-family: var(--mono);
  text-align: right;
  white-space: nowrap;
}
.inspect-table__type code {
  background: none;
  border: 0;
  padding: 0;
  font-weight: 700;
}
.inspect-badge {
  margin-left: 0.35rem;
  padding: 0 0.25rem;
  border: 1px solid var(--gray-dark);
  font-size: 0.65rem;
  color: var(--gray-dark);
}
.inspect-fields {
  display: grid;
  grid-template-columns: minmax(8rem, auto) 1fr;
  gap: 0.3rem 0.9rem;
  margin-top: 0.75rem;
  font-size: 0.82rem;
}
.inspect-field__label {
  font-weight: 700;
}
.inspect-field__value {
  font-family: var(--mono);
  overflow-wrap: anywhere;
}

/* --- Transcode chip (file row) --- */
.transcode-chip {
  border-bottom: 2px dotted currentColor;
  cursor: help;
}
.transcode-chip--exact {
  color: var(--ok);
}
.transcode-chip--coefficients {
  color: var(--gray-dark);
}

/* --- Processing Status --- */
#processing-status,
#intake-notice {
  text-align: center;
  padding: 0.5rem;
  margin-bottom: 1rem;
  font-weight: 600;
}

/* --- Utilities --- */
.hidden {
  display: none !important;
}

/* --- Dark Mode --- */
@media (prefers-color-scheme: dark) {
  :root {
    --black: #f0f0f0;
    --white: #1a1a1a;
    --gray: #333;
    --gray-dark: #999;
    --accent: #62c6f2;
    --accent-dark: #80d4f7;
    /* Overridden here for the same reason the light value moved: --warn is now
       #a62f00, which is 6.6:1 on the light background and 1.7:1 on this one.
       A token that has a light value and no dark one is a token that fails in
       whichever mode nobody looked at. */
    --warn: #ff8a5c;
    --ok: oklch(0.75 0.18 140);
  }
  body {
    background: #1a1a1a;
    color: #f0f0f0;
  }
  .card--warning {
    background: #2a1a10;
  }
  #drop-zone.drag-over {
    background: #1a2a35;
  }
  .disabled-reason-box {
    background: #2a1a10;
  }
}

/* --- Mobile --- */
@media (max-width: 480px) {
  /* 28px is a comfortable target with a mouse and a miss with a thumb. The row
     grows to fit, which is the correct trade on the one device where this help
     is most needed and a title= tooltip would have shown nothing. */
  .explain-btn {
    width: 44px;
    height: 44px;
    font-size: 1.05rem;
  }
  .license-inventory ul {
    columns: 1;
  }
  .container {
    padding: 1rem 0.75rem;
  }
  header h1 {
    font-size: 2.25rem;
  }
  .lang-selector {
    width: 4.1rem;
    height: 1.75rem;
  }
  .lang-picker-current {
    border-width: 1px;
    font-size: 0.72rem;
    gap: 0.16rem;
  }
  .lang-picker-flag {
    font-size: 0.9rem;
  }
  #drop-zone {
    padding: 2rem 1rem;
  }
  .file-item {
    align-items: stretch;
    flex-direction: column;
    gap: 0.75rem;
  }
  /* auto-fit rather than a fixed count: there are five actions now and the
     labels below them are translated, so the row that fits three in English
     fits two in German. Wrapping is correct; overflowing the card is not.
     6.5rem rather than 5.5rem because 5.5 was the width at which the longest
     label in the table — "Technische Details", "Технические детали" — broke
     across two lines, and 88px tracks are what "cramped" meant. Measured with
     scripts/measure-mobile-actions.mjs across 320/360/390/480 and five
     locales, which is where those numbers come from. */
  .file-actions {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
    gap: 0.5rem;
  }
  /* `height: auto` is the fix here, and `min-height` is only the floor under it.
     The base rule gives every one of these a definite `height: 2.75rem`, and a
     min-height cannot raise a box that already has one — so a label that needed
     a second line was painted outside the border and over the row beneath it,
     which is the reported bug. Growing the box keeps the 44px touch target as a
     minimum rather than as a ceiling. */
  .file-actions button {
    width: 100%;
    height: auto;
    min-height: 44px;
    padding: 0.35rem 0.3rem;
    gap: 0.25rem;
    flex-direction: column;
  }
  .file-actions button::after {
    content: attr(title);
    font-size: 0.7rem;
    line-height: 1.15;
    /* Break inside a word before breaking out of the button: hyphens are off,
       so "Herunterladen" in a narrow track would otherwise run out sideways. */
    overflow-wrap: anywhere;
  }
  .inspect-panel {
    width: 100%;
    border-left: 0;
    padding: 1rem 0.85rem;
  }
  .inspect-fields {
    grid-template-columns: 1fr;
    gap: 0.1rem 0;
  }
  .inspect-field__value {
    margin-bottom: 0.4rem;
  }
}
