/* Image tools — shared layout.
   Colour, radius and type come from /tools/shared/site-theme.css, and the metrics
   deliberately match /tools/pdf/shared/pdf.css: the two suites sit side by side on
   the same site and must not look like they were built by different people.
   The type ramp is documented at the top of pdf.css and is the same seven steps
   here; the surface ladder is documented at .pdf-controls there. */

* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; max-width: 100%; }
body {
  background: var(--bg, #fff); color: var(--ink, #0a0a0a);
  font-family: var(--sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui, sans-serif);
  -webkit-font-smoothing: antialiased;
}
a { color: var(--accent); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }

/* header shell — same markup as every other tool so the toolkit can wire it */
.pg-header {
  display: flex; align-items: center; gap: 16px; padding: 0 18px; min-height: 64px;
  background: var(--bg); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 10;
}
.pg-brand { display: flex; align-items: center; gap: 12px; flex: 1 1 auto; min-width: 0; }
.pg-logo { display: inline-flex; flex: 0 0 auto; }
.pg-title { font-weight: 700; font-size: 15px; letter-spacing: -0.01em; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pg-header-actions { display: flex; gap: 6px; flex: 0 0 auto; }
.pg-icon-btn {
  display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px;
  border-radius: 8px; border: 1px solid var(--line); background: var(--surface); color: var(--muted);
  cursor: pointer; text-decoration: none;
}
.icon-moon { display: none; }
:root[data-theme="dark"] .icon-sun { display: none; }
:root[data-theme="dark"] .icon-moon { display: inline; }
:root[data-theme="light"] .icon-sun { display: inline; }
:root[data-theme="light"] .icon-moon { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .icon-sun { display: none; }
  :root:not([data-theme]) .icon-moon { display: inline; }
}

.img-main { max-width: 900px; margin: 0 auto; padding: 28px 20px 56px; }
/* the page's opening line, as a standfirst in the display face — see .pdf-lede */
.img-lede {
  margin: 0 0 24px;
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 1.7vw, 1.3rem);
  line-height: 1.4;
  letter-spacing: -0.01em;
  color: var(--ink);
  max-width: 46ch;
}
.img-main:has(> .img-hub) > .img-lede { font-size: clamp(1.2rem, 2vw, 1.5rem); max-width: 54ch; }

/* ---- drop zone ---------------------------------------------------------- */
/* --surface-2 so the target has a fill in BOTH themes — see .pdf-drop */
.img-drop {
  /* --line-strong for the edge — a 2px dashed --line measured 1.26:1 on the light
     page. See the longer note at .pdf-drop; stated identically so the two suites
     stay the same code. */
  border: 2px dashed var(--line-strong, var(--line)); border-radius: var(--radius-md, 1rem);
  background: var(--surface-2); padding: 36px 24px; text-align: center; cursor: pointer;
  transition: border-color .2s, background .2s, transform .2s;
}
.img-drop:hover, .img-drop:focus-visible { border-color: var(--accent); }
.img-drop.is-drag { border-color: var(--accent); background: var(--accent-soft); transform: scale(1.01); }
.img-drop-icon {
  width: 60px; height: 60px; margin: 0 auto; border-radius: 18px;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface); border: 1px solid var(--line);
  font-size: 28px; line-height: 1;
}
.img-drop-title { margin: 14px 0 4px; font-weight: 700; font-size: 1.05rem; }
.img-drop-sub { margin: 0; color: var(--faint); font-size: .88rem; }
.img-drop input[type=file] { display: none; }

/* collapse the chooser once a picture is loaded — see the note at .pdf-drop.
   #editor is the container all three image tools unhide. */
.img-main:has(#editor:not([hidden])) .img-drop {
  /* flex-start once collapsed, so the populated page reads on one left edge —
     see the note in pdf.css */
  display: flex; align-items: center; justify-content: flex-start; gap: 12px;
  padding: 13px 20px; text-align: left;
}
.img-main:has(#editor:not([hidden])) .img-drop-icon {
  width: 36px; height: 36px; border-radius: 11px; font-size: 18px; margin: 0; flex: 0 0 auto;
}
/* the mark comes down with the tile — see the note in pdf.css */
.img-main:has(#editor:not([hidden])) .img-drop-icon svg { width: 20px; height: 20px; }
.img-main:has(#editor:not([hidden])) .img-drop-title { margin: 0; font-size: .95rem; }
.img-main:has(#editor:not([hidden])) .img-drop-sub { margin: 0; }
/* not on a phone — see the note in pdf.css */
@media (max-width: 640px) {
  .img-main:has(#editor:not([hidden])) .img-drop-sub { display: none; }
}

/* the promise, tinted with the brand rather than filed under small print —
   see the note at .pdf-privacy */

/* The lock is a drawn mark now, not 🔒. The emoji was the last one left in the
   suites and it sat in the one band the design deliberately tints: a gold object
   in an indigo panel, from a different visual language than every other mark on
   the page. It is a sibling of the text, not inside it, because data-i18n
   rewrites textContent and would wipe an inline child on the first language switch. */
.img-privacy {
  margin: 12px 0 0; padding: 11px 14px; border-radius: var(--radius-sm, .5rem);
  background: var(--accent-soft); border: 1px solid transparent;
  color: var(--muted); font-size: .88rem;
  display: flex; align-items: flex-start; gap: 8px;
}
/* nudged down by the cap-height difference so the mark sits on the first line,
   not above it; flex-none so a long sentence never squeezes it oval */
.tool-privacy-icon { width: 16px; height: 16px; flex: 0 0 auto; margin-top: 2px; }

/* ...and steps back once a picture is in: the promise has been made and kept, and
   a full-chroma band above the work outranks the work. See the note in pdf.css.
   #editor is the container all three image tools unhide, as above. */
.img-main:has(#editor:not([hidden])) .img-privacy {
  background: transparent; border-color: var(--line); color: var(--muted);
}

/* ---- controls ------------------------------------------------------------ */
/* --shadow gives the panel its middle rung of the surface ladder in light mode,
   where --surface and --bg are the same white; see the note at .pdf-controls.
   18px between groups rather than 14: a field is label(6px)control(6px)hint, so
   at 14 the gap BETWEEN two fields was barely twice the gap inside one, and on
   Compress Image — where every hint is a two- or three-line paragraph — the hint
   appeared to belong equally to the control above it and the label below it. */
.img-controls {
  margin: 22px 0 0; padding: 18px; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius-md, 1rem);
  box-shadow: var(--shadow);
  display: flex; flex-direction: column; gap: 18px;
}
.img-field { display: flex; flex-direction: column; gap: 6px; }
/* same reason as pdf.css: a group of fields wrapped in a bare <div> to be toggled
   inherits no gap and sits flush against the field above it */
.img-controls > div:not([class]) { display: flex; flex-direction: column; gap: 18px; }
.img-field label { font-weight: 600; font-size: .88rem; }
.img-hint { color: var(--faint); font-size: .82rem; }
.img-input, .img-select {
  width: 100%; padding: 10px 12px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
  font-family: inherit; font-size: .95rem; outline: none;
}
.img-input:focus-visible, .img-select:focus-visible {
  border-color: var(--accent);
  outline: 2px solid var(--accent); outline-offset: 2px;
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.img-input::placeholder { color: var(--faint); opacity: 1; }
/* A colour <input> is still an .img-input in the markup, so it inherited the
   text field's full width and 12px padding: on Convert the "Pick" swatch drew as
   a 282x44 box with a thin white bar inside it, which reads as an empty text
   field rather than as the colour you chose. Size it like a swatch and let the
   colour itself fill the box. Still 64x44, so it stays a legal touch target. */
.img-input[type=color] { width: 64px; padding: 4px; cursor: pointer; }
/* Chrome's UA sheet puts `margin: 2px` on a range input. pdf.css leaves the
   width to flex stretch, which subtracts that margin; adding `width: 100%` here
   did not, so the slider started 2px right of every select in the same panel
   (measured: select 229-511, slider 231-513) and pushed its .img-field 2px wider
   than its own content box. Zero the side margins and it lines up again. */
.img-controls input[type=range] {
  accent-color: var(--accent); width: 100%; margin-left: 0; margin-right: 0;
}
.img-radio-row { display: flex; flex-wrap: wrap; gap: 10px; }
.img-radio {
  display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 999px; background: var(--surface-2);
  font-size: .88rem; font-weight: 600; color: var(--muted);
}
.img-radio input { accent-color: var(--accent); }
/* A RADIO pill is one of a set, so the fill is what says which one — it keeps the
   gradient. --cta-gradient, not --accent-gradient: the pill carries white
   14.4px/600 text, and --accent-gradient's own stops measure 4.47 / 4.23 / 3.53
   against white — sampled off the rendered pill, the label sat on 3.97-4.38:1,
   under AA everywhere along the ramp. --cta-gradient is the site's darkened ramp
   for exactly this case (6.29 / 5.70 / 6.04). */
.img-radio:has(input[type=radio]:checked) {
  border-color: transparent; background: var(--cta-gradient, var(--accent-gradient)); color: #fff;
}
/* A CHECKBOX pill is a standalone switch, and at full CTA gradient it out-shouted
   the button that does the work — on Resize, "Keep the proportions" was the most
   saturated thing on the screen while the Resize button sat below the fold. One
   gradient per view; see the longer note at .pdf-radio. Measured on the rendered
   pill: 4.67:1 light, 6.55:1 dark. */
.img-radio:has(input[type=checkbox]:checked) {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
}
/* two numeric fields that belong together (width x height) */
.img-pair { display: grid; grid-template-columns: 1fr auto 1fr; gap: 8px; align-items: end; }
/* The x has to sit on the centre-line of the two boxes it joins. A fixed
   padding-bottom cannot track their height: at 11px the glyph measured 5.5px
   below the input's centre on a desktop (40px fields) and 7.5px below it on a
   phone, where the touch rule at the bottom of this file grows them to 44px.
   Give the x the field's own height and centre inside it instead — both are
   bottom-aligned in the grid row, so equal heights means equal centres. */
.img-pair-x {
  display: flex; align-items: center; justify-content: center;
  height: 40px; padding: 0; color: var(--faint);
}

.img-actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-top: 22px; }
.img-btn {
  appearance: none; border: 0; cursor: pointer; padding: 12px 22px; border-radius: var(--radius-md, 1rem);
  background: var(--cta-gradient, var(--accent-gradient)); color: #fff;
  font-family: inherit; font-size: 1rem; font-weight: 700; letter-spacing: .3px;
  box-shadow: 0 4px 15px rgba(99, 102, 241, .4); transition: transform .15s, box-shadow .15s;
}
.img-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(99, 102, 241, .5); }
/* The filled disabled state is defined once in site-theme.css, beside the filled
   enabled state; a ghost button has no fill to ruin, so dimming it is fine. */
.img-btn-ghost:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
.img-btn-ghost {
  background: transparent; color: var(--muted); border: 1px solid var(--line); box-shadow: none; font-weight: 600;
}
.img-btn-ghost:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); transform: none; box-shadow: none; }
.img-btn-sm { padding: 8px 14px; font-size: .88rem; border-radius: 9px; flex: 0 0 auto; }
/* A finished batch showed one full-gradient Download per row plus a bigger
   full-gradient "Download all (ZIP)" — four identical CTAs in one screen, with
   nothing saying which one gets you everything. Rank them: the per-row button is
   a real button, in the accent, and the ZIP button keeps the ramp. (The single
   Download in the PDF suite's result panel is the only action there, so it stays
   primary — that is not the same case.) */
.img-row:not(.img-row-zip) .img-btn-sm {
  background: var(--accent-soft); color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.img-row:not(.img-row-zip) .img-btn-sm:hover:not(:disabled) {
  background: var(--accent); color: var(--accent-ink); box-shadow: none;
}
/* A ghost button used as a toggle (the Flip pair on Resize) had no pressed state,
   so a mirrored picture looked exactly like an unmirrored one — and for a
   symmetrical picture the preview is no clue either. Keyed on aria-pressed rather
   than an id, so any tool that adds a toggle gets it. The inset ring keeps it
   readable once the pointer moves away. */
.img-btn[aria-pressed="true"] {
  border-color: var(--accent); color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* --good / --bad are theme-aware tokens in site-theme.css now, so every
   hand-written dark twin in this file is gone — including the three the comment
   at .img-badge records as having been forgotten once already. */
.img-status { min-height: 22px; margin: 14px 2px 0; font-size: .95rem; }
.img-status.ok { color: var(--good, #06744a); }
.img-status.err { color: var(--bad, #b91c1c); }
.img-busy { display: flex; align-items: center; gap: 10px; margin-top: 14px; color: var(--muted); font-size: .95rem; }
.img-spinner {
  width: 18px; height: 18px; border-radius: 50%; flex: 0 0 auto;
  border: 2px solid var(--line); border-top-color: var(--accent); animation: imgspin .8s linear infinite;
}
@keyframes imgspin { to { transform: rotate(360deg); } }
.img-progress { height: 8px; border-radius: 999px; overflow: hidden; margin: 12px 0 0;
  background: var(--surface-2); border: 1px solid var(--faint); }
/* --cta-gradient: a progress fill is a filled brand surface, and that is the ramp
   every other filled brand surface on these pages uses — see the note in pdf.css */
.img-progress > span { display: block; height: 100%; width: 0;
  background: var(--cta-gradient, linear-gradient(135deg,#6366f1,#8b5cf6,#ec4899));
  transition: width .18s ease; }

/* ---- results list -------------------------------------------------------- */
.img-list { list-style: none; margin: 22px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
/* --shadow for the same reason .pdf-file takes one: the ladder says a --surface
   panel gets the site's card shadow, and in light --surface is the page colour, so
   a finished batch was a stack of white cards on white held by a 1.26:1 hairline.
   See the note in pdf.css. */
.img-row {
  display: flex; align-items: center; gap: 12px; padding: 10px 12px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm, .5rem);
  box-shadow: var(--shadow);
}
.img-row.is-failed { border-color: var(--bad, #b91c1c); }
/* the "download everything" row has no thumbnail or metadata, so without this it
   renders as an empty card with a button floated to one side */
/* ...but not the "download everything" row: it is deliberately not a card (no
   thumbnail, no metadata), and giving it the card shadow would have promoted it
   back level with the result rows the ranking above works to separate it from. */
.img-row-zip { justify-content: center; background: transparent; border-style: dashed; box-shadow: none; }
/* the checkerboard shows transparency honestly — a PNG with alpha turned into a
   JPEG loses it, and this is where that becomes visible before downloading */
.img-row-thumb {
  flex: 0 0 auto; width: 48px; height: 48px; border-radius: 6px; overflow: hidden;
  display: flex; align-items: center; justify-content: center; border: 1px solid var(--line);
  background: var(--surface-2);
}
/* The checkerboard belongs on the PICTURE, not on the box around it. On the box
   it filled the letterbox as well, so a 1600x1200 photo.jpg — fully opaque —
   showed the same checkerboard bands above and below it that alpha.png shows
   through its transparent corners, and a failed row with no thumbnail at all
   rendered as a plain checkerboard square, i.e. as a fully transparent image.
   The whole point of the pattern is to be believable, so paint it behind the
   image itself: max-width/max-height sizes that box to exactly the visible
   picture, so the pattern can only ever show where the pixels really are
   see-through. Light grey in both themes on purpose: it is the convention every
   image editor uses, and pdf.css grounds its page thumbs on #fff for the same
   reason — the swatch is a property of the picture, not of the theme. */
.img-row-thumb img, .img-row-thumb canvas {
  max-width: 100%; max-height: 100%; display: block;
  background-color: #fff;
  background-image: linear-gradient(45deg, #e6e6e6 25%, transparent 25%, transparent 75%, #e6e6e6 75%),
                    linear-gradient(45deg, #e6e6e6 25%, transparent 25%, transparent 75%, #e6e6e6 75%);
  background-size: 12px 12px; background-position: 0 0, 6px 6px;
}
.img-row-meta { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; gap: 2px; }
.img-row-name { font-weight: 600; font-size: .95rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.img-row-sub { color: var(--faint); font-size: .82rem; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.img-row-sub strong { color: var(--ink); font-weight: 700; }
.img-row-sub.is-err { color: var(--bad, #b91c1c); white-space: normal; }
.img-row-arrow { color: var(--faint); }
/* The badge is the one place that does NOT take the shared semantic tokens, and
   the exception is deliberate. Both halves of it are tuned as a pair against each
   other: 12.5px/700 ink on a tint, which needs 4.5:1 in its own right. Swapping
   the tint for --good-soft (a darker green at .14 rather than this lighter one at
   .16) dropped the measured ratio from 4.70:1 to 4.37:1 — under AA. --good/--bad
   themselves are tuned against --bg and would land near 3.5:1 on a pill. So the
   badge keeps its four measured values; only the dark-mode INK needs a twin,
   because the tint is light-grey-green in both themes on purpose. */
.img-badge { font-size: .78rem; font-weight: 700; padding: 2px 7px; border-radius: 999px; letter-spacing: .2px; }
.img-badge.good { background: rgba(53,192,138,.16); color: #0b7a52; }  /* 4.70:1 on the tinted pill */
.img-badge.bad { background: rgba(240,102,107,.16); color: #a52a2f; }  /* 6.0:1 */
:root[data-theme="dark"] .img-badge.good { color: #35c08a; }            /* 6.0:1 */
:root[data-theme="dark"] .img-badge.bad { color: #f0666b; }             /* 4.8:1 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .img-badge.good { color: #35c08a; }
  :root:not([data-theme]) .img-badge.bad { color: #f0666b; }
}

/* ---- editor: controls beside the picture -------------------------------- */
/* 340px, not 320: pdf.css has used 340 for the same two-column editor all along,
   so an image tool and a PDF tool opened side by side had control columns 20px
   apart for no reason anyone could name. The header of this file claims the
   metrics match pdf.css; now they do. */
.img-editor { display: grid; gap: 20px; grid-template-columns: minmax(0, 340px) minmax(0, 1fr);
  align-items: start; margin-top: 22px; }
/* .img-controls carries `margin: 22px 0 0` for when it stands alone under the
   drop zone. Inside the two-column editor that margin is a column offset, and
   with align-items:start it is not absorbed: the controls panel started 22px
   lower than the picture beside it on all three tools (measured 425.0 vs 403.0
   at 1280). pdf.css zeroes it the same way, unconditionally — the ≤900px block
   below used to be the only place it was cleared, which is why the desktop
   layout was the broken one. */
.img-editor > .img-controls { margin-top: 0; }
.img-stage {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md, 1rem);
  box-shadow: var(--shadow);
  padding: 14px; display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.img-stage canvas, .img-stage img { max-width: 100%; height: auto; display: block; border-radius: 6px; }
/* Not a caption. On all three image tools this line is the live readout of what
   the tool is about to do to your picture — "photo.jpg — 189.7 KB becomes 91.9 KB
   (52% smaller)" — i.e. the answer to the question the page exists to answer, and
   at .82rem/--faint it was the quietest text on the screen while the file name in
   the collapsed chooser above it was louder. One step up, to the size and ink the
   rest of the page uses for secondary content. Same move, and the same reason, as
   #docInfo in pdf.css; no new step enters the type ramp. */
.img-caption { color: var(--muted); font-size: .88rem; text-align: center; margin: 0; }

/* ---- before / after comparison ------------------------------------------ */
/* One picture on top of the other with a draggable divider, rather than two side
   by side: at the sizes a phone can show, side-by-side is too small to judge, and
   judging the damage is the entire point of a quality slider. */
.img-compare { position: relative; width: 100%; max-width: 100%; overflow: hidden;
  border-radius: 6px; touch-action: pan-y; user-select: none; }
.img-compare img { display: block; width: 100%; height: auto; }
/* object-fit:fill, not contain. The clip-path the script sets is a fraction of
   this ELEMENT's box, while contain letterboxes the image inside that box
   whenever the two pictures' aspect ratios differ by even a rounding step — and
   they can, because "Also make it smaller" re-derives the height from a rounded
   width. Any letterbox slides the after picture sideways relative to the before
   one while the seam stays put, so the comparison would be showing two shifted
   copies of the same scene and quietly lying about the damage. fill pins the
   after image to exactly the before image's box, so every column matches. */
.img-compare .img-cmp-after { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: fill; clip-path: inset(0 0 0 50%); }
/* The script sets `left` on the line and the grip and the same percentage as the
   clip inset. left puts the line's LEFT EDGE on the seam, so a 2px line hung
   entirely on the compressed side — measured seam 245.0, line centre 246.0,
   grip centre 245.0 at every width and every position. Pull it back by half its
   width so the seam, the line and the grip are the same pixel. */
.img-compare .img-cmp-line { position: absolute; top: 0; bottom: 0; left: 50%; width: 2px;
  margin-left: -1px;
  background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,.35); pointer-events: none; }
.img-compare .img-cmp-grip {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 34px; height: 34px; border-radius: 999px; background: #fff; color: #111;
  display: flex; align-items: center; justify-content: center; font-size: 15px;
  box-shadow: 0 2px 10px rgba(0,0,0,.4); pointer-events: none;
}
.img-compare:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.img-cmp-tags { display: flex; justify-content: space-between; width: 100%;
  font-size: .82rem; color: var(--faint); font-weight: 600; }

/* The editors keep the picture first in the markup so focus order matches what is
   on screen (WCAG 2.4.3): on a phone the controls sit under it, and a keyboard
   user tabbing out of the drop zone used to jump down the page and then back up.
   On a wide screen the controls belong on the left — a purely visual swap, at the
   one size where both columns are visible at once. */
@media (min-width: 901px) {
  .img-editor > .img-controls { order: 1; }
  .img-editor > :not(.img-controls) { order: 2; }
}

@media (max-width: 900px) {
  .img-editor { grid-template-columns: 1fr; }
  /* No `order` here any more: the markup puts the stage first, so the phone
     layout and the tab order agree. The desktop rule below does the columns. */
}
/* 640px and a 16px gutter, matching pdf.css exactly. At 560/14px an image page
   and a PDF page opened side by side at ~600px wide had different gutters and
   one had stacked action buttons while the other still had them in a row —
   small, but it is the kind of drift that makes two suites read as two authors. */
@media (max-width: 640px) {
  .img-main { padding: 20px 16px 44px; }
  .img-actions .img-btn { flex: 1 1 100%; }
}
/* a result row at 320px runs out of room exactly as a PDF file row does */
@media (max-width: 400px) {
  .img-row { flex-wrap: wrap; }
  .img-row .img-btn-sm { flex: 1 0 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .img-drop, .img-btn, .img-progress > span { transition: none; }
  .img-spinner { animation: none; }
  /* killing the transition still left the lift itself, so a hover or a drag-over
     still jumped the element — which is the part motion sensitivity is about */
  .img-btn:hover:not(:disabled) { transform: none; }
  .img-drop.is-drag { transform: none; }
}
/* a finger needs 44px; emulated mobile still reports a fine pointer, so the
   width half of this query is the half that is actually testable */
@media (pointer: coarse), (max-width: 760px) {
  .img-input, .img-select, .img-radio, .img-btn { min-height: 44px; }
  .img-controls input[type=range] { height: 44px; }
  .img-btn-sm { min-height: 44px; }
  /* the fields grow, so the x between them has to grow with them or it stops
     being on their centre-line — see .img-pair-x above */
  .img-pair-x { height: 44px; }
}

/* ---- a plain note that explains what a tool will do before it does it ---- */
.img-note {
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: 8px; padding: 12px 14px; margin: 0 0 4px;
  font-size: .88rem; color: var(--muted); line-height: 1.55;
  background: var(--surface-2);
}

/* ---- hub grid ------------------------------------------------------------ */
.img-hub { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); margin-top: 26px; }
.img-card {
  display: flex; flex-direction: column; gap: 6px; padding: 20px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md, 1rem);
  box-shadow: var(--shadow);
  text-decoration: none; color: inherit; transition: border-color .2s, transform .2s, box-shadow .2s;
}
.img-card:hover { border-color: var(--accent); transform: translateY(-3px); box-shadow: var(--shadow), var(--shadow-glow); }
/* the same tile as .pdf-card-icon — the note there explains why the emoji cannot
   carry a grid on their own. These three are 🗜️ 🔄 📐: a black clamp, a blue
   keycap and a grey set square. */
.img-card-icon {
  width: 46px; height: 46px; border-radius: 14px; align-self: flex-start;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); border: 1px solid var(--line);
  font-size: 24px; line-height: 1; margin-bottom: 6px;
}
.img-card:hover .img-card-icon { border-color: var(--accent); }
.img-card-name { font-weight: 700; font-size: 1.05rem; }
.img-card-desc { color: var(--muted); font-size: .88rem; line-height: 1.5; }
.img-hub-note { margin-top: 26px; color: var(--faint); font-size: .88rem; }
/* One card per row on a phone, so the stacked desktop card spent a whole line on
   a 46px tile with nothing beside it — a 15-tool hub was 2,600px of scrolling.
   Put the tile on the title's line; the description still owns the column under
   it, and the list gets about a fifth shorter.
   This block has to live AFTER the .img-card rules above, not up in the general
   ≤640px section near the top of the file: a media query adds no specificity, so
   `.img-card { display: grid }` there was silently losing to `.img-card
   { display: flex }` here — leaving a flex column that had picked up
   `align-items: center` from the losing rule and centred every card's title. */
@media (max-width: 640px) {
  .img-card { display: grid; grid-template-columns: auto 1fr; column-gap: 14px; row-gap: 4px; align-items: center; }
  .img-card-icon { margin-bottom: 0; }
  .img-card-desc { grid-column: 2; }
}
@media (prefers-reduced-motion: reduce) { .img-card { transition: none; } .img-card:hover { transform: none; } }

/* ---- About / SEO section ------------------------------------------------ *
   All four image pages ship the same <section class="tool-about"> markup as the
   PDF tools — same classes, same order, down to .about-faq and .about-foot —
   but nothing was styling it. img.css never had these rules, toolkit.css does
   not carry them, and site-theme.css only re-points the --ab-* tokens and adds
   the display face; the rules that USE those tokens live in pdf.css, which
   these pages do not load. So below the tool every image page fell back to
   browser defaults: full-bleed text at 1280px instead of an 860px column, no
   64px band, the highlight card and the FAQ accordion rendered as bare <div>
   and <details>, the feature list as disc bullets instead of green ticks, and
   the CTA as an underlined link with a gradient stuck behind the text. It was
   the single loudest "bolted on" signal left on the suite.

   Ported from pdf.css unchanged so the two suites stay literally the same code:
   any future fix to one is a copy-paste to the other. The --ab-* tokens come
   from /tools/shared/site-theme.css, which loads after this file and overrides
   the dark values below with the tool palette, exactly as it does for the PDF
   pages.                                                                     */
.tool-about {
  background: var(--ab-bg); color: var(--ab-text); border-top: 1px solid var(--ab-border);
  padding: 64px 24px 80px; font-size: 15px;
}
:root[data-theme="dark"] .tool-about {
  --ab-bg: #0f131a; --ab-surface: #1a1f2b; --ab-text: #e8eaf2;
  --ab-muted: #b7bccd; --ab-faint: #99a1b3; --ab-border: #262a38; --ab-accent: #8d89f6;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .tool-about { --ab-bg: #0f131a; --ab-surface: #1a1f2b; --ab-text: #e8eaf2;
    --ab-muted: #b7bccd; --ab-faint: #99a1b3; --ab-border: #262a38; --ab-accent: #8d89f6; }
}
.tool-about-inner { max-width: 860px; margin: 0 auto; line-height: 1.65; }
/* Split exactly as in pdf.css — the note there explains why: .tool-about h1 is
   the case where the About headline IS the page's h1, .about-hero-h is the case
   where an h1 already sits in the hero above and this must read as a step down
   from it (28px/700 at 1440, against the hero's 37.6px/700). */
.tool-about h1 { font-size: clamp(1.6rem, 3.5vw, 2.2rem); font-weight: 800; letter-spacing: -0.02em; line-height: 1.15; color: var(--ab-text); }
.tool-about .about-hero-h { font-size: clamp(1.4rem, 2.5vw, 1.75rem); font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; color: var(--ab-text); }
/* --mono is not defined on these pages, and a bare var() with no fallback makes
   the whole declaration invalid — that is how inline <code> silently rendered in
   the sans body face on three of the PDF pages. Keep the fallback. */
.tool-about code { font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); font-size: 0.88em; background: var(--ab-surface); padding: 1px 6px; border-radius: 5px; color: var(--ab-text); }
.about-lede { font-size: 1.05rem; color: var(--ab-muted); margin-top: 14px; max-width: 68ch; }
.about-cta-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 22px; }
.about-btn { display: inline-flex; align-items: center; padding: 10px 18px; border-radius: 10px; background: var(--ab-accent); color: #fff; text-decoration: none; font-weight: 600; font-size: 0.95rem; transition: transform .15s ease, box-shadow .15s ease; }
.about-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 18px -6px var(--ab-accent); }
.about-btn.ghost { background: transparent; color: var(--ab-accent); border: 1px solid var(--ab-border); }
.about-btn:focus-visible, .about-links a:focus-visible, .about-faq summary:focus-visible { outline: 2px solid var(--ab-accent); outline-offset: 2px; border-radius: 6px; }
.about-highlight { margin: 40px 0; padding: 22px 24px; background: var(--ab-surface); border: 1px solid var(--ab-border); border-left: 4px solid var(--ab-accent); border-radius: 12px; }
.about-highlight h2 { font-size: 1.15rem; margin: 0 0 8px; color: var(--ab-text); }
.about-highlight p { color: var(--ab-muted); margin: 0; }
.about-h2 { font-size: 1.3rem; font-weight: 700; margin: 40px 0 16px; color: var(--ab-text); }
.about-features { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(2,1fr); gap: 12px 28px; }
.about-features li { position: relative; padding-left: 26px; color: var(--ab-muted); font-size: 0.95rem; }
.about-features li::before { content: "\2713"; position: absolute; left: 0; top: 0; color: #15803d; font-weight: 700; }
:root[data-theme="dark"] .about-features li::before { color: #35c08a; }
@media (prefers-color-scheme: dark) { :root:not([data-theme]) .about-features li::before { color: #35c08a; } }
.about-features strong { color: var(--ab-text); }
.about-faq { display: flex; flex-direction: column; gap: 10px; }
.about-faq details { border: 1px solid var(--ab-border); border-radius: 10px; background: var(--ab-surface); overflow: hidden; }
.about-faq summary { cursor: pointer; padding: 14px 18px; font-weight: 600; color: var(--ab-text); list-style: none; position: relative; }
.about-faq summary::-webkit-details-marker { display: none; }
.about-faq summary::after { content: "+"; position: absolute; right: 18px; color: var(--ab-accent); font-weight: 700; }
.about-faq details[open] summary::after { content: "\2013"; }
.about-faq details p { padding: 0 18px 16px; margin: 0; color: var(--ab-muted); }
.about-links ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; }
.about-links li { color: var(--ab-muted); }
.about-links a { color: var(--ab-accent); font-weight: 600; text-decoration: none; }
.about-links a:hover { text-decoration: underline; }
.about-foot { margin-top: 44px; padding-top: 20px; border-top: 1px solid var(--ab-border); font-size: 0.88rem; color: var(--ab-faint); }
.about-foot a { color: var(--ab-accent); text-decoration: none; }
@media (max-width: 640px) { .about-features { grid-template-columns: 1fr; } .tool-about { padding: 48px 18px 64px; } }

/* ---- forced-colors (Windows High Contrast) ------------------------------- *
   Measured in this mode: .img-badge.good and .img-badge.bad resolved to the same
   ink on the same ground, so "52% smaller" and "183% larger" became visually
   identical; .img-row.is-failed lost its red border; and the comparison divider
   and its grip both forced to black, which vanishes against a dark photograph.
   Colour cannot be relied on here, so each distinction is restated in shape. */
@media (forced-colors: active) {
  .img-badge { border: 1px solid currentColor; forced-color-adjust: none; }
  .img-badge.good { color: CanvasText; background: Canvas; }
  .img-badge.bad  { color: Mark;       background: Canvas; }
  /* a failed row needs a boundary that survives the colour substitution */
  .img-row.is-failed { outline: 2px solid Mark; outline-offset: -2px; }
  /* the divider is the whole point of the comparison — keep it drawn */
  .img-compare .img-cmp-line, .img-compare .img-cmp-grip { forced-color-adjust: none; }
  .img-status.ok::before { content: '\2713\00a0'; }   /* ✓ — state, not colour */
  .img-status.err::before { content: '\26a0\00a0'; }  /* ⚠ */
}

/* ---- icons -------------------------------------------------------------- *
   One drawn family replaces the emoji, which came from three different visual
   languages and rendered muddy at tile size on a dark panel — the clamp and the
   ruler in particular read as a missing glyph rather than an icon. Stroked in
   the accent so the tile has brand presence, and sized here because font-size
   does nothing to an SVG. */
/* the one thing on an empty page you are meant to act on — see pdf.css */
.img-drop-icon { display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft); border-color: transparent; }
/* 30px in a 60px tile. The three tiles in this family are 38/21 on a hub card
   and 54/27 in the hero — 55% and 50% of the tile — and the drop tile was the
   biggest tile in the set carrying the smallest mark in it, at 26/60 = 43%. So
   the one control the empty page exists for held the emptiest-looking tile.
   Same 50% as the hero. */
.img-drop-icon svg { width: 30px; height: 30px; color: var(--accent); }
/* Tinted, not grey — the site's own card language; see the note at
   .pdf-card-icon in pdf.css. The two suites share the /tools/ wall, so this has
   to be the same treatment on both or the one page that shows them together
   would show two. */
.img-card-icon { display: flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 11px;
  background: var(--accent-soft); border: 1px solid transparent; }
.img-card-icon svg { width: 21px; height: 21px; color: var(--accent); }
/* the card lifts on hover; the mark should follow it rather than sit still */
.img-card:hover .img-card-icon { border-color: var(--accent); }

/* The hubs are grids of cards, not a column of prose, so the 900px measure that
   suits a tool page just left a third of a wide screen empty and squeezed 15
   cards into 3 columns. Give the card walls room; the tool pages keep the column.

   :nth-child(8), not a bare .img-hub — and this file is where that matters.
   1180px of `auto-fill, minmax(240px,1fr)` is four columns, and /tools/image/
   has exactly three cards: the widening bought its only row a 285px hole and put
   240px of empty page either side of it, i.e. the rule made the page it was
   meant to help worse. Below the gate that page sits at 900px, where three cards
   are a complete row. /tools/ still qualifies on its 15-card PDF wall. Stated
   identically in pdf.css so the two files stay the same code.

   Live editors take the same measure for a different reason: at 900px the
   preview gets ~500px after the 340px control column, so the before/after
   comparison you are supposed to judge the compression on is a third the size of
   the picture. #editor is hidden until a file loads, so an empty page keeps the
   reading column. */
@media (min-width: 1120px) {
  .img-main:has(.img-hub > :nth-child(8)),
  .img-main:has(.img-editor:not([hidden])) { max-width: 1180px; }
}

/* Group headings on the all-tools hub. They were borrowing .about-h2, which is
   scoped to the About band, so in <main> they fell back to a browser default and
   ended up SMALLER than the display-face standfirst above them — the hierarchy
   read upside down. */
.hub-group {
  margin: 52px 0 4px; font-family: var(--font-display, Georgia, serif);
  font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.01em; font-weight: 700;
  color: var(--ink);
}
/* This rule was written to put the section's intro a step below its heading and
   was silently doing nothing: `.img-main:has(> .img-hub) > .img-lede` above
   scores 0,3,0 (:has() takes the specificity of its argument) and a bare
   `.hub-group + .img-lede` scores 0,2,0, so every group intro on /tools/ rendered
   at the full 24px hub standfirst size — the same size as the heading above it
   AND as the page's own hero lede, three levels of the page set in one size.
   Qualify it to 0,3,0 so it wins on source order, and drop it to the UI body
   size in --muted: a heading you can see is a heading needs the line under it to
   be prose, not a second headline. */
.img-main > .hub-group + .img-lede {
  font-size: .95rem; line-height: 1.55; color: var(--muted);
  max-width: 62ch; margin-bottom: 4px;
}
