/* PDF tools — shared layout.
   Colour, radius and type come from /tools/shared/site-theme.css, so these pages
   inherit the site identity instead of inventing another one.

   TYPE RAMP — eight steps, and nothing between them. This file and img.css used
   to carry fifteen distinct sizes (.74 .78 .8 .82 .84 .86 .88 .9 .92 .95 .98
   1.02 1.04 1.05 1.15 1.3rem, plus a stray 13px), seven of them inside the one
   band between .86 and 1.05, and no two neighbours far enough apart to read as
   different. That is the tell of sizes chosen one control at a time. The
   survivors, and what each is FOR:
       .78rem  micro — the result badges, the before/after tags
       .82rem  fine print — hints, sub-lines, file sizes, captions
       .88rem  secondary — field labels, descriptions, notes, small buttons
       .95rem  UI body — inputs, file names, status messages
       1rem    primary action label
       1.05rem object title — a card name, a drop-zone heading
       1.15rem sub-heading inside the About band
       1.3rem  section heading in the About band
       clamp() the page's opening line, and the About H1
   The About band additionally sets its own 15px root, which is deliberate: it is
   an editorial column, not chrome, and its em-based sizes hang off that.
   Adding a size means retiring one.                                         */

* { 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 the other tools 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; }
}

.pdf-main { max-width: 900px; margin: 0 auto; padding: 28px 20px 56px; }
/* Every page in both suites opened on a 16.3px grey sentence and nothing else:
   below the 18px header wordmark there was no headline anywhere above the fold,
   and the page's real H1 — set in Georgia, with the site gradient — lives at the
   bottom, inside the About band, where nobody who came to merge a PDF will
   scroll. So the lede is the only thing that can carry the page's voice at the
   top, and it was carrying it in the same face and weight as a form hint.
   Set it as a standfirst in the display face instead: same words, same place,
   but the page now opens the way a page of mehditmimi.com opens. --ink rather
   than --muted because a standfirst is content, not chrome. */
.pdf-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;
}
/* On the hub it is the whole promise of the suite and there is no tool below it
   competing for the eye, so let it run one step larger. */
.pdf-main:has(> .pdf-hub) > .pdf-lede { font-size: clamp(1.2rem, 2vw, 1.5rem); max-width: 54ch; }

/* ---- drop zone ---------------------------------------------------------- */
/* --surface-2, not --surface. In light mode --surface and --bg are both #ffffff,
   so the first thing every one of the 18 tools showed you was a dashed rectangle
   with no fill at all — 860x171 of page colour, held together by a 10%-black
   hairline. A drop target has to look like a place you can put something, and it
   has to look like that in both themes: --surface-2 is #f5f5f5 on the white page
   and #1e1e1e on the black one, so it now reads as an area either way. The
   drag-over cue still changes (accent border + --accent-soft over the fill). */
.pdf-drop {
  /* --line-strong, not --line. The fill above gives the target a body; the edge
     is what says where it ENDS, and a 2px dashed --line measured 1.26:1 against
     the light page and 1.40:1 against the dark one — the primary control of the
     empty state, outlined in a line you cannot see. This file already made that
     exact argument for the signature pad twelve rules down and then drew that
     one boundary at a fixed .45 ink; --line-strong is the same reasoning
     promoted to a token (3:1, WCAG 1.4.11). Hover/drag still step UP to the full
     accent, so the affordance keeps its two-stage feedback. */
  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;
}
.pdf-drop:hover, .pdf-drop:focus-visible { border-color: var(--accent); }
.pdf-drop.is-drag { border-color: var(--accent); background: var(--accent-soft); transform: scale(1.01); }
/* The emoji sits in a tinted tile — see the note at .pdf-card-icon for why the
   emoji themselves cannot be trusted to have comparable weight or colour. */
.pdf-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;
}
.pdf-drop-title { margin: 14px 0 4px; font-weight: 700; font-size: 1.05rem; }
.pdf-drop-sub { margin: 0; color: var(--faint); font-size: .88rem; }
.pdf-drop input[type=file] { display: none; }

/* Once the tool has a document, the chooser has done its job and should stop
   being the biggest object on the page. Measured on merge-pdf at 1280: the box
   held 171px of empty dashed rectangle ABOVE the file list, the controls and the
   button, on every subsequent visit to the page; on compress-image it pushed the
   primary button to y=894, i.e. off a 900px screen. Collapse it to a single
   64px row — still a full-width target, still keyboard-focusable, just no longer
   shouting. Keyed on the containers the tools unhide when a file loads, since
   none of them sets a class we could use.

   Three ids, not five. #editorWrap and #controls were in the list and both fired
   on an EMPTY page:
     - jpg-to-pdf's #editorWrap is a permanent wrapper around a hidden #controls
       and a hidden #fileList, so it carries no [hidden] of its own and matched
       from first paint;
     - page-numbers ships #controls with no [hidden] INSIDE a hidden #editorWrap,
       so the id matched even though nothing was on screen.
   The result was that two of the eighteen tools opened with the chooser already
   collapsed to a 66px bar — no drop target at all on the one screen whose whole
   job is to accept a file — and with the privacy note already stepped back to a
   hairline, losing the brand tint exactly where the rule below argues it is
   worth most. Neither id is needed: every tool that unhides #controls also
   unhides #docInfo or #fileList (page-numbers unhides #docInfo; merge-pdf and
   jpg-to-pdf unhide #fileList), so the shorter list covers all fifteen and
   matches nothing before a document exists. Verified page by page. */
.pdf-main:has(#docInfo:not([hidden]), #fileList:not([hidden]),
              #editor:not([hidden])) .pdf-drop {
  /* flex-start, not center. Collapsed, this is no longer the page's invitation —
     it is a secondary control sitting above the document you are actually
     working on, and every other thing in the column (the privacy line, #docInfo,
     the editor, the buttons) is set from the left edge. Centring ~250px of label
     inside an 1140px bar left ~445px of empty dashed box on each side and gave
     the populated page a second, competing axis. One left edge, top to bottom. */
  display: flex; align-items: center; justify-content: flex-start; gap: 12px;
  padding: 13px 20px; text-align: left;
}
.pdf-main:has(#docInfo:not([hidden]), #fileList:not([hidden]),
              #editor:not([hidden])) .pdf-drop-icon {
  width: 36px; height: 36px; border-radius: 11px; font-size: 18px; margin: 0; flex: 0 0 auto;
}
/* ...and the mark has to come down with the tile it sits in. The full-size tile is
   60px carrying a 30px mark; collapsed it is 36px, and leaving the mark at 30 put
   it 83% across its own tile — edge to edge, where every other tile in the family
   sits at 50-55%. 20 in 36 is 56%, the same proportion as the 21-in-38 hub card. */
.pdf-main:has(#docInfo:not([hidden]), #fileList:not([hidden]),
              #editor:not([hidden])) .pdf-drop-icon svg { width: 20px; height: 20px; }
.pdf-main:has(#docInfo:not([hidden]), #fileList:not([hidden]),
              #editor:not([hidden])) .pdf-drop-title { margin: 0; font-size: .95rem; }
.pdf-main:has(#docInfo:not([hidden]), #fileList:not([hidden]),
              #editor:not([hidden])) .pdf-drop-sub { margin: 0; }
/* ...but not on a phone. There is nothing to drag FROM on a touch device, so in
   the compact row "or drop them here" is dead words — and at 320px it broke the
   row into two ragged two-line columns ("Choose / files" beside "or drop them /
   here"). The full-size empty state still says it, where there is room. Same
   selector, later in the file, so it actually wins: :has() takes the specificity
   of its argument, which a bare .pdf-drop-sub cannot match. */
@media (max-width: 640px) {
  .pdf-main:has(#docInfo:not([hidden]), #fileList:not([hidden]),
                #editor:not([hidden]), #editorWrap:not([hidden]),
                #controls:not([hidden])) .pdf-drop-sub { display: none; }
}

/* ---- privacy note: the whole point of this suite ------------------------ */
/* It was drawn as the most ignorable object on the page: --surface-2 grey fill,
   --muted text, hairline border — the visual language of a cookie banner. But
   "nothing is uploaded" is the entire reason this suite exists and the one thing
   a visitor arriving from a search result needs to believe. Tint it with the
   brand instead, so it reads as a promise the site is making rather than
   small print it is hiding behind. --muted on --accent-soft measures 8.3:1 in
   light and 7.4:1 in dark. Not a status colour: green here would read as a
   result, and the result has not happened yet. */

/* 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. */
.pdf-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 it steps back once a document is in. The tint was argued for the EMPTY
   state, where "nothing is uploaded" is the one thing a visitor arriving from a
   search result needs to believe. Once a file is loaded that promise has been
   made and kept, and the band was still the highest-chroma object on the page —
   measured on watermark-pdf at 1440 the eye went hero -> purple band -> button,
   with the document you had just opened quieter than the reassurance about it.
   Same words, same place, no longer competing: hairline instead of fill, --muted
   instead of the tint (8.6:1 light / 11:1 dark on the page). Same selector list
   as the drop-zone collapse above, for the same reason — no tool sets a class. */
.pdf-main:has(#docInfo:not([hidden]), #fileList:not([hidden]),
              #editor:not([hidden])) .pdf-privacy {
  background: transparent; border-color: var(--line); color: var(--muted);
}

/* ---- file list ----------------------------------------------------------- */
.pdf-files { list-style: none; margin: 20px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
/* --shadow, because the ladder documented at .pdf-controls says a --surface panel
   gets one and this is the last --surface surface in the file that never did. In
   dark that is free (black on black) and the tonal step #161616-on-#0a0a0a was
   already doing the work; in light --surface and --bg are both #ffffff, so the
   documents you have loaded — the actual content of the page — were white cards on
   a white page held together by a 1.26:1 hairline, sitting UNDER a controls panel
   that did have the shadow and did read as an object. The content should not be
   the least substantial thing on the page. */
.pdf-file {
  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);
  cursor: grab;
}
.pdf-file.is-dragging { opacity: .5; }
.pdf-file.is-over { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.pdf-file-grip { color: var(--faint); cursor: grab; flex: 0 0 auto; }
.pdf-file-meta { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.pdf-file-name { font-weight: 600; font-size: .95rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-file-sub { color: var(--faint); font-size: .82rem; }
/* #docInfo borrows .pdf-file-sub, but it is not a caption under a row — it is the
   only place a working page names the document you are editing, sitting alone
   between the privacy note and the editor. At .82rem/--faint it was the faintest
   text on the page: on watermark-pdf you could read the reassurance about your
   file more easily than the name of the file. One step up, to the size and ink
   the rest of the page uses for secondary content. Nine PDF tools carry it and
   none of them sets a class of its own, which is why this keys on the id — the
   drop-zone collapse above already does the same for the same reason. */
/* 14px above and 22px below grouped the document's name with the privacy note
   it sits under rather than the editor it describes. It was an inline style on
   twelve files; it belongs here, and the spacing now leans the other way. */
#docInfo { color: var(--muted); font-size: .88rem; margin: 20px 0 8px; }
.pdf-file-acts { display: flex; gap: 4px; flex: 0 0 auto; }
.pdf-icon-act {
  appearance: none; width: 30px; height: 30px; border-radius: 7px; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--muted); font-size: 12px;
}
.pdf-icon-act:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.pdf-icon-act:disabled { opacity: .35; cursor: default; }

/* ---- controls ------------------------------------------------------------ */
/* The three-tier surface ladder these pages are built on, stated once:
     page      --bg          flat
     panel     --surface     + --shadow   (controls, stage, preview host, cards)
     recessed  --surface-2   (inputs, the drop target, the result band)
   In dark that ladder is tonal and already worked: #0a0a0a → #161616 → #1e1e1e.
   In light it did not exist — --bg and --surface are BOTH #ffffff, so a panel
   was page-coloured and the input inside it was DARKER than its own container,
   i.e. the stack ran white → white → grey. Light mode was not the authored
   inverse of dark, it was the ladder with its middle rung missing.
   --shadow is the site's own card shadow (identical values to --shadow-sm plus
   --shadow-md in /css/style.css) and it is theme-aware: in light it lifts the
   panel off the white page exactly as the home page lifts its tool cards, and
   in dark it is black-on-black and does nothing, which is correct — there the
   tonal step is already doing the work. */
.pdf-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;
}
.pdf-field { display: flex; flex-direction: column; gap: 6px; }
/* .pdf-controls spaces its own children by 14px, but the tools that switch between
   modes wrap each mode's fields in a plain <div> (#drawFields, #textFields, …).
   Those nested fields inherited no gap at all and sat flush — on Sign PDF the "Pen
   thickness" label touched the hint above it (measured 0.0px), and Watermark PDF
   had the same 0.0px seam in #textFields. Re-state the rhythm one level down so a
   wrapped group is spaced like an unwrapped one. */
.pdf-controls > div:not([class]) { display: flex; flex-direction: column; gap: 18px; }
.pdf-field label { font-weight: 600; font-size: .88rem; }
/* Not `.pdf-field .pdf-hint`: merge-pdf and jpg-to-pdf both use .pdf-hint for
   their standalone empty-state line, which sits outside any .pdf-field, so the
   class did nothing there and each page patched it with an inline
   style="color:var(--faint)". img.css never scoped its .img-hint. Unscope it and
   the two suites use the same selector for the same thing again. */
.pdf-hint { color: var(--faint); font-size: .82rem; }
.pdf-input, .pdf-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;
}
/* The `outline: none` above also killed the global :focus-visible ring, so the only
   focus cue left on a field was a 1px border tint plus a 3px --accent-soft halo,
   and that halo composites to 1.09:1 on a light panel / 1.15:1 on a dark one —
   i.e. no visible focus at all on the Unlock password box, the page-range fields
   or the watermark text. Put back the same 2px accent outline every other control
   on these pages already uses; the soft halo stays as a fill cue. */
.pdf-input:focus-visible, .pdf-select:focus-visible {
  border-color: var(--accent);
  outline: 2px solid var(--accent); outline-offset: 2px;
  box-shadow: 0 0 0 3px var(--accent-soft);
}
/* Placeholders were left to the UA. Chrome resolves them to ~#757575 in BOTH
   themes, which measured 4.23:1 on the light field and 3.62:1 on the dark one —
   under AA, and not a site colour either. --faint is the token for de-emphasised
   text and clears AA on both fills (4.9:1 / 4.7:1). opacity:1 because Firefox
   dims placeholders on top of whatever colour you give them. */
.pdf-input::placeholder, .pdf-select::placeholder { color: var(--faint); opacity: 1; }
/* A colour <input> carries .pdf-input in the markup, so on Watermark PDF and
   Sign PDF it inherited the text field's full width and 12px padding and drew as
   a 302x44 box with a thin bar of the chosen colour inside it — which reads as an
   empty text field, not as a swatch. img.css fixed this months ago; the PDF
   suite never got the same rule. Size it as a swatch and let the colour fill it.
   Still 64x44, so it stays a legal touch target. */
.pdf-input[type=color] { width: 64px; padding: 4px; cursor: pointer; }
/* Range inputs were left entirely to the UA, so they rendered Chrome-blue in a
   column of otherwise indigo controls — precisely the "bolted on" read the shared
   tokens exist to prevent. accent-color is all it takes; the rest of the native
   slider is fine as it is.
   The margins are Chrome's UA `margin: 2px`, which flex stretch subtracts:
   measured on Watermark PDF, the selects ran 229-531 and every slider 231-533,
   so the whole panel had two right-hand edges. img.css zeroes them for the same
   reason — this is the other half of that fix. */
.pdf-controls input[type=range] { accent-color: var(--accent); margin-left: 0; margin-right: 0; }
.pdf-radio-row { display: flex; flex-wrap: wrap; gap: 10px; }
.pdf-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);
}
.pdf-radio input { accent-color: var(--accent); }
/* A RADIO pill is one of a set and the fill is what tells you which one is
   chosen, so it keeps the gradient. White 600-weight text on --accent-gradient
   measured 4.47 / 4.23 / 3.53:1 along its three stops — below AA everywhere, and
   worst at the pink end. --cta-gradient is the darkened ramp the site already
   uses behind white button text, and gives 6.29 / 5.70 / 6.04:1 for what looks
   like the same colour. */
.pdf-radio:has(input[type=radio]:checked) { border-color: transparent; background: var(--cta-gradient, var(--accent-gradient)); color: #fff; }
/* A CHECKBOX pill is not. All five of them in the two suites are standalone
   `width:fit-content` switches — "Add a marker before each page", "Keep the text
   searchable", "Repeat across the page", "Keep the proportions" — and giving them
   the same full CTA gradient as the primary button meant the loudest object on
   the page was a checkbox. On PDF to Text it was literally the only thing in the
   controls panel, drawn in the site's most saturated fill, while the button that
   actually does the work sat below it in the same colour at half the width. One
   gradient per view: the pill states "on" in the accent, the button keeps the
   ramp. Measured on the rendered pill, --accent on --accent-soft composited over
   the panel gives 4.67:1 light and 6.55:1 dark, and the native tick is already
   accent-coloured. */
.pdf-radio:has(input[type=checkbox]:checked) {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
}

.pdf-actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-top: 22px; }
.pdf-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;
}
.pdf-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, next to the
   filled enabled state — see the note there on why `opacity: .5` on a gradient
   is the wrong tool. A ghost button has no fill to ruin, so dimming is fine. */
.pdf-btn-ghost:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
.pdf-btn-ghost {
  background: transparent; color: var(--muted); border: 1px solid var(--line); box-shadow: none; font-weight: 600;
}
.pdf-btn-ghost:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); transform: none; box-shadow: none; }

/* --good / --bad are theme-aware tokens in site-theme.css now, so the hand-written
   dark twin that used to follow each of these rules is gone. The fallbacks stay
   for the same reason .tool-about code keeps one: a bare var() with no fallback
   invalidates the whole declaration if the token ever fails to load. */
.pdf-status { min-height: 22px; margin: 14px 2px 0; font-size: .95rem; }
.pdf-status.ok { color: var(--good, #06744a); }
.pdf-status.err { color: var(--bad, #b91c1c); }
.pdf-busy { display: flex; align-items: center; gap: 10px; margin-top: 14px; color: var(--muted); font-size: .95rem; }
.pdf-spinner {
  width: 18px; height: 18px; border-radius: 50%; flex: 0 0 auto;
  border: 2px solid var(--line); border-top-color: var(--accent); animation: pdfspin .8s linear infinite;
}
@keyframes pdfspin { to { transform: rotate(360deg); } }

/* ---- page grid ----------------------------------------------------------- */
.pdf-grid {
  display: grid; gap: 12px; margin-top: 20px;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.pdf-page {
  /* --line-strong for the same reason .pdf-drop takes it: this is a control — you
     click it to select, drag it to reorder, focus it and move it with the arrow
     keys — and its boundary is the only thing that says where one page ends and
     the next begins. The thumbnail inside is rendered on #fff, so in light mode a
     2px --line frame put a white page inside a 1.26:1 box on a white page and an
     8-page document read as a loose scatter of pale rectangles. Hover still steps
     up to the full accent and selection is still the accent ring, so nothing about
     the state ladder changes — only the rung it starts from. */
  position: relative; border: 2px solid var(--line-strong, var(--line)); border-radius: 10px; overflow: hidden;
  background: var(--surface-2); cursor: pointer; aspect-ratio: 3 / 4;
  display: flex; align-items: center; justify-content: center; transition: border-color .15s, transform .15s;
}
.pdf-page:hover { border-color: var(--accent); transform: translateY(-2px); }
.pdf-page.is-selected { border-color: transparent; box-shadow: 0 0 0 3px var(--accent); }
.pdf-page.is-removed { opacity: .35; }
.pdf-page.is-removed::after {
  content: '✕'; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 30px; color: var(--bad, #b91c1c); font-weight: 700;
}
.pdf-page-thumb { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.pdf-page-thumb canvas, .pdf-page-thumb img { max-width: 100%; max-height: 100%; display: block; background: #fff; }
.pdf-page-no {
  position: absolute; bottom: 4px; right: 6px; font-size: 11px; font-weight: 700;
  background: rgba(0,0,0,.65); color: #fff; padding: 1px 6px; border-radius: 5px;
}
.pdf-page-rot { position: absolute; top: 4px; left: 6px; font-size: 11px; font-weight: 700;
  background: var(--accent); color: #fff; padding: 1px 6px; border-radius: 5px; }

/* ---- results ------------------------------------------------------------- */
/* This is the payoff — the file you came for — and it was drawn as the quietest
   band on the page: a flat --surface-2 strip with a hairline, sitting below a
   full-strength gradient "Merge PDFs" button that had already done its job. Once
   a result exists it should be the thing your eye lands on. An accent border
   plus the site's own --shadow-glow (the same treatment the hub cards get on
   hover) is enough to say "here it is" without adding another filled colour to a
   view that already has a Download button in it. */
.pdf-result {
  margin-top: 22px; padding: 16px; border-radius: var(--radius-md, 1rem);
  border: 1px solid var(--accent); background: var(--surface);
  box-shadow: var(--shadow-glow), var(--shadow);
  display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
}
.pdf-result-list { list-style: none; margin: 0; padding: 0; width: 100%; display: flex; flex-direction: column; gap: 8px; }
.pdf-result-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.pdf-result-name { font-weight: 600; font-size: .95rem; flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-result-size { color: var(--faint); font-size: .82rem; }

@media (max-width: 640px) {
  .pdf-main { padding: 20px 16px 44px; }
  .pdf-grid { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
  .pdf-actions .pdf-btn { flex: 1 1 100%; }
}
/* At 320px a file row ran out of room: three 44px touch targets, an 8px gap
   between each, the grip and the 34px page thumbnail take 230px of a 262px row,
   which left the file name 32 pixels. Measured on merge-pdf with two documents
   loaded, "plain.pdf" rendered as "pl…" and "3 pages · 1.2 KB" broke over four
   lines. Give the name the whole width and send the buttons to their own line —
   they stay 44px and stay in reach of a thumb. */
@media (max-width: 400px) {
  .pdf-file { flex-wrap: wrap; }
  .pdf-file-acts { flex: 1 0 100%; justify-content: flex-end; }
}
@media (prefers-reduced-motion: reduce) {
  .pdf-drop, .pdf-page, .pdf-btn { transition: none; }
  .pdf-spinner { animation: none; }
}

/* ---- hub ---------------------------------------------------------------- */
.pdf-hub { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); margin-top: 26px; }
.pdf-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;
}
/* keep the resting lift under the glow, or the card DROPS as it rises on hover */
.pdf-card:hover { border-color: var(--accent); transform: translateY(-3px); box-shadow: var(--shadow), var(--shadow-glow); }
/* The 18 emoji across the two hubs have nothing in common: 🔗 and 📐 render as
   thin grey line art, 🔀 🔄 #️⃣ as saturated blue keycap tiles, ✂️ ✍️ 🏷️ as full-
   colour objects. Side by side in a grid they read as a ransom note, and there is
   no CSS that can make an emoji weigh what its neighbour weighs. What CSS can do
   is stop the emoji being the frame: put every one of them in the same tile, and
   the grid regains a rhythm — the eye lines up the tiles and reads the emoji as
   contents. This is also the home page's own card language (/#tools draws its
   PDF, image and playground icons in exactly such a tile). The tile doubles as
   the grouping cue the card was missing: icon-to-name was 6px and name-to-desc
   was 6px, so the three parts read as one undifferentiated stack. */
.pdf-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;
}
.pdf-card:hover .pdf-card-icon { border-color: var(--accent); }
.pdf-card-name { font-weight: 700; font-size: 1.05rem; }
.pdf-card-desc { color: var(--muted); font-size: .88rem; line-height: 1.5; }
.pdf-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 .pdf-card rules above, not up in the general
   ≤640px section near the top of the file: a media query adds no specificity, so
   `.pdf-card { display: grid }` there was silently losing to `.pdf-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) {
  .pdf-card { display: grid; grid-template-columns: auto 1fr; column-gap: 14px; row-gap: 4px; align-items: center; }
  .pdf-card-icon { margin-bottom: 0; }
  .pdf-card-desc { grid-column: 2; }
}
@media (prefers-reduced-motion: reduce) { .pdf-card { transition: none; } .pdf-card:hover { transform: none; } }


/* ---- About / SEO section ------------------------------------------------ *
   Same markup and treatment as the other tools; the --ab-* tokens come from
   /tools/shared/site-theme.css so this matches the site automatically.        */
/* About section — self-contained WCAG-AA palette (independent of theme toggle) */
.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; }
/* .tool-about h1 is the dev tools' case, where the About headline IS the page's
   only h1 and correctly the biggest type on the page. */
.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); }
/* .about-hero-h is not that. Since the hero landed, this is an H2 opening an
   editorial section BELOW a page that already has an h1 — and it was still
   wearing the h1's clothes: measured at 1440 it set 35.2px at weight 800 in the
   brand gradient against a hero h1 of 33.6px at 700 in flat ink, so the page's
   own title was the smaller and plainer of the two. Step it down (28px / 700 at
   1440) and the ramp reads 37.6 -> 28 -> 20.8 (.about-h2) -> 18.4
   (.about-highlight h2). It keeps the gradient: that is what marks where the
   editorial band starts, and at 28px/700 it is still large text for contrast. */
.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 — so inline <code> was silently rendering in the
   sans body face on page-numbers, remove-pages and split-pdf. */
.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; } }

/* ---- live preview ------------------------------------------------------- *
   Configuring a watermark from sliders alone is guesswork; this shows the real
   page with the effect drawn on top, in the same coordinates the save path uses. */
/* --surface, matching .pdf-controls beside it and .img-stage in the other suite.
   On --surface-2 the two halves of the editor sat on different rungs of the
   ladder, and which one looked raised flipped with the theme: in dark, #1e1e1e
   next to #161616 made the preview the elevated pane; in light, #f5f5f5 next to
   #ffffff made it the recessed one. Two sibling panes in one grid row should be
   the same surface in both themes. */
.pv-host {
  margin-top: 22px; padding: 14px; border-radius: var(--radius-md, 1rem);
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: var(--shadow);
}
.pv-bar { display: flex; align-items: center; justify-content: center; gap: 14px; margin-bottom: 12px; }
/* --surface-2, the recessed rung. Every other control that sits INSIDE a panel on
   these pages is on --surface-2 — .pdf-input, .pdf-select, .pdf-radio, .pdf-icon-act
   (the same small button, in the file list) — and this pair was the one exception,
   drawn in the same --surface as the .pv-host panel behind it. That leaves the page
   arrows carrying no tonal step at all in EITHER theme: #ffffff on #ffffff in light,
   #161616 on #161616 in dark, held apart by a 1.26/1.40:1 hairline. They are the
   only way through a multi-page document with a keyboard or a mouse. */
.pv-nav {
  appearance: none; width: 34px; height: 34px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
  font-size: 18px; line-height: 1;
}
.pv-nav:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.pv-nav:disabled { opacity: .35; cursor: default; }
.pv-label { font-family: var(--mono, monospace); font-size: .82rem; color: var(--muted); min-width: 62px; text-align: center; }
.pv-stage { display: flex; justify-content: center; }
.pv-canvas {
  display: block; max-width: 100%; height: auto;
  border-radius: 6px; background: #fff;
  /* var(--shadow), not a fixed rgba(0,0,0,.35): that value was chosen so a white
     page would sit ON the dark background instead of looking like a hole punched
     in it, and it does that well — but in light mode it painted a heavy grey halo
     around a white sheet on a white panel, the only hard shadow anywhere on the
     light page. --shadow is the site's card shadow and is already theme-aware:
     .06/.08 black in light, .4/.5 in dark. Same intent, authored per theme.

     ...but --shadow alone left the light page with NO page. .pv-host is --surface
     and in light --surface, --bg and this sheet are all #ffffff, so the document
     you are signing, cropping or watermarking was white paper on a white panel on
     a white page, and --shadow's .06/.08 blur gave it no visible edge — measured
     in light there was no boundary between the sheet and the panel anywhere except
     under the bottom blur. You cannot place a mark at a margin you cannot see.
     Add a hairline ring, and make it a fixed ink rather than --line for exactly the
     reason `background: #fff` above is fixed and the note at .sig-pad spells out:
     the edge of a sheet of paper is a property of the paper, not of the theme.
     .45 is 3.16:1 measured against white — and it is deliberately the same ink
     .sig-pad already uses for the same argument, so the file carries one value for
     "the edge of a sheet of paper" rather than two. In dark it is a near-black line at the
     join between a white sheet and a #161616 panel, where an 18:1 edge already
     exists — invisible there, which is correct. A ring, not a border: this element
     is a <canvas> sized from its attributes, so a real border would change layout. */
  box-shadow: 0 0 0 1px rgba(10, 10, 10, .45), var(--shadow);
  cursor: grab;
}
.pv-canvas.is-dragging { cursor: grabbing; }
/* the canvas is focusable so the mark can be nudged with the arrow keys — that
   focus has to be visible, and its announcements readable only by screen readers */
.pv-canvas:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.pv-live { 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; }
.pv-hint { margin: 10px 0 0; text-align: center; color: var(--faint); font-size: .82rem; }

/* editor layout: controls beside the preview on wide screens.
   22px, not 20px: it is the same "first block under the file" position that
   .pdf-controls occupies on the tools without a preview, and img.css uses 22px
   for its own editor. Three numbers for one gap is how two suites drift apart. */
.pdf-editor { display: grid; gap: 20px; grid-template-columns: minmax(0, 340px) minmax(0, 1fr); align-items: start; margin-top: 22px; }
.pdf-editor > .pdf-controls { margin-top: 0; }
/* ...and the same for the pane on the other side of the row. `.pv-host` carries
   `margin-top: 22px` for when a preview stands alone under the file, but inside
   the grid that margin is a column offset, and with align-items:start it is not
   absorbed: measured on watermark-pdf at 1440 the controls panel started at
   y=403 and the preview panel beside it at y=425. Two panes in one row have to
   share a top edge. It is zeroed unconditionally rather than only on the desktop
   query because when the editor stacks, `.pdf-editor`'s own 22px margin is
   already the gap — the second 22 was doubling it there too. */
.pdf-editor > * > .pv-host { margin-top: 0; }
@media (max-width: 900px) {
  .pdf-editor { grid-template-columns: 1fr; }
  /* Stacked, the controls are ~515px tall, which pushed the page being edited
     670px below the fold on a phone: you were dragging sliders at a preview you
     could not see. Put the page first and the controls under it — only where
     there is a live preview to look at (.pdf-editor-live). */
  /* No `order` needed here any more: the markup itself now puts the preview
     first, so the phone layout and the tab order finally agree. See the
     desktop rule below for the two-column arrangement. */
}

/* The editors keep the preview first in the markup so that focus order matches
   what is on screen (WCAG 2.4.3): on a phone the controls sit under the picture,
   and previously a keyboard user tabbing out of the drop zone jumped ~650px down
   the page and then back up. On a wide screen the controls belong on the left,
   which is what `order` is for — a purely visual rearrangement, at the one size
   where both columns are visible at once so the jump cannot happen. */
@media (min-width: 901px) {
  .pdf-editor-live > .pdf-controls { order: 1; }
  .pdf-editor-live > :not(.pdf-controls) { order: 2; }
}

/* image-watermark picker */
.pdf-imgpick { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.pdf-imgpick img { height: 46px; width: auto; border-radius: 6px; border: 1px solid var(--line); background: #fff; }
.pdf-imgpick input[type=file] { display: none; }

/* thumbnail inside a file row */
.pdf-file-thumb {
  flex: 0 0 auto; width: 34px; height: 44px; border-radius: 4px; overflow: hidden;
  background: #fff; border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
}
.pdf-file-thumb img, .pdf-file-thumb canvas { max-width: 100%; max-height: 100%; display: block; }

/* ---- touch targets ------------------------------------------------------ *
   The row controls (▲ ▼ ✕) and the preview page arrows are 30px and 34px, which
   is below the 44px minimum. That matters most on a phone, where those buttons
   are the ONLY way to reorder a merge — HTML5 drag-and-drop does not work on
   touch. Keep the compact look for mouse users; grow the hit area for fingers. */
/* Triggered by width as well as pointer type: a narrow viewport is overwhelmingly
   a phone, and unlike `pointer: coarse` it is something the test rig can actually
   exercise — a rule that cannot be verified is a rule that quietly rots. */
@media (pointer: coarse), (max-width: 760px) {
  .pdf-icon-act { width: 44px; height: 44px; font-size: 15px; }
  .pv-nav { width: 44px; height: 44px; font-size: 20px; }
  .pdf-file-acts { gap: 8px; }
  /* Measured on a phone-width viewport: .pdf-input came out 40px tall, .pdf-select
     42px and the .pdf-radio pill 39px — all just under the minimum, and the
     Unlock password box is the first thing a finger has to hit. */
  .pdf-input, .pdf-select, .pdf-radio { min-height: 44px; }
  /* The primary .pdf-btn measured 42px: padding 12+12 over an ~18px line box, with
     border:0 — the ghost variant only reaches 44 because of its 1px border. */
  .pdf-btn { min-height: 44px; }
  /* A native range is 16px tall, so Sign PDF's pen-thickness and size sliders were
     250x16 hit areas. Grow the box only: the track still draws centred inside it,
     so the slider looks exactly the same and only the target grows. */
  .pdf-controls input[type=range] { height: 44px; }
}
/* On a fine pointer the boxes stay small, but the clickable area does not: a
   pseudo-element extends the target without changing the layout. */
@media (pointer: fine) {
  .pdf-icon-act, .pv-nav { position: relative; }
  .pdf-icon-act::after, .pv-nav::after {
    content: ''; position: absolute; top: 50%; left: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%);
  }
}

/* ---- signature pad (Sign PDF) --------------------------------------------
   Always a white sheet with a ruled baseline, in both themes: you are signing
   paper, and ink on a dark pad would not look like the signature you get. */
.sig-pad {
  display: block; width: 100%; height: auto; max-width: 100%;
  aspect-ratio: 640 / 220;
  background: #fff;
  /* The edge cannot come from --line. That token is tuned to sit on a themed
     panel, but this element is always a white sheet, so --line ends up composited
     over white: it measured 1.25:1 in light and 1.00:1 in dark — a literally
     invisible border, which left the "sign in the box" box with no box, and on the
     dark page made it read as a broken white rectangle rather than paper. A fixed
     ink tint is right here for the same reason `background: #fff` above is: it is a
     property of the paper, not of the theme. 3.15:1 on white, so the boundary
     clears the 3:1 required of a control's visual boundary. */
  border: 1px dashed rgba(10, 10, 10, .45); border-radius: 8px;
  /* ...and ground the sheet the way .pv-canvas grounds the page preview, so in
     dark mode it sits ON the page instead of looking like a hole punched in it.
     Theme-aware for the same reason .pv-canvas is — see the note there. */
  box-shadow: var(--shadow);
  cursor: crosshair; touch-action: none;
  /* the rule sits where a signature line would, at ~72% of the height; at .28
     alpha it was 1.47:1 on the white pad and all but vanished, .45 is 1.84:1 and
     still reads as a guide rather than as content */
  background-image: linear-gradient(to right, rgba(99,102,241,.45) 0 100%);
  background-size: calc(100% - 48px) 1px;
  background-position: 24px 72%;
  background-repeat: no-repeat;
}
.sig-pad:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

/* ---- crop overlay (Crop PDF) ---------------------------------------------- */
.pv-canvas.is-crop { cursor: crosshair; }
.crop-readout {
  display: grid; grid-template-columns: auto 1fr; gap: 4px 12px;
  font-size: .82rem; color: var(--muted); margin: 0;
}
.crop-readout dt { color: var(--faint); }
.crop-readout dd { margin: 0; font-variant-numeric: tabular-nums; }

/* ---- per-page progress (Unlock PDF) --------------------------------------- */
.pdf-progress {
  height: 8px; border-radius: 999px; overflow: hidden; margin: 12px 0 0;
  background: var(--surface-2);
  /* --line composited over the page measured 1.25:1 in light and 1.42:1 in dark,
     so the empty part of the bar was invisible and a part-done unlock read as a
     short gradient dash floating in nothing — you could not see what it was a
     fraction OF. A 1px --faint rail is >5:1 in both themes and still reads as a
     quiet capsule. box-sizing:border-box (set on * above) keeps the outer height
     at 8px, so the fill stays the 6px it was. */
  border: 1px solid var(--faint);
}
/* --cta-gradient, not --wordmark-gradient. --wordmark-gradient exists for one
   job: type clipped out of a gradient on a light ground, where the ordinary
   stops fall under AA. A progress fill is a filled brand surface, which is what
   --cta-gradient is for, and what every button and every selected pill on these
   pages already uses. Three ramps in circulation is one more than the design has
   uses for, and the odd one out was the bar that reports progress. */
.pdf-progress > span {
  display: block; height: 100%; width: 0;
  background: var(--cta-gradient, linear-gradient(135deg,#6366f1,#8b5cf6,#ec4899));
  transition: width .18s ease;
}
/* This has to live AFTER the rule above, not up in the main reduced-motion block:
   a media query adds no specificity, so at equal specificity the later
   declaration wins and the block up there was silently losing. The fill
   re-animates once per page, so a 40-page unlock is 40 slides. */
@media (prefers-reduced-motion: reduce) {
  .pdf-progress > span { transition: none; }
  /* Removing a transition leaves the movement — it just arrives instantly instead
     of gliding, which is still the motion someone asked not to see. Measured: with
     reduce emulated, a drag-over still settled at matrix(1.01,…). Both rules need
     their full selector to outrank the one being overridden, and this block is
     last in the file so source order is on its side. */
  .pdf-btn:hover:not(:disabled) { transform: none; }
  .pdf-drop.is-drag { transform: none; }
  /* measured with reduce emulated and a real hover: both of these still lifted */
  .pdf-page:hover { transform: none; }
  .about-btn:hover { transform: none; }
}
/* a plain note that explains what a tool will do before it does it */
.pdf-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);
}

/* ---- forced-colors (Windows High Contrast) ------------------------------- *
   .pdf-status.ok and .pdf-status.err differ only in colour, so in this mode the
   line that says a password was wrong reads exactly like the one that says the
   file is ready. Restate it as a glyph, which survives the substitution. */
@media (forced-colors: active) {
  .pdf-status.ok::before { content: '\2713\00a0'; }   /* ✓ */
  .pdf-status.err::before { content: '\26a0\00a0'; }  /* ⚠ */
  /* .pdf-btn has border:0 and its gradient collapses to the page colour here —
     measured body and button both rgb(0,0,0) — so the primary action loses its
     boundary entirely (1.4.11). */
  .pdf-btn { border: 1px solid ButtonText; }
  /* selection is carried by a box-shadow, which forced-colors discards */
  .pdf-page.is-selected { outline: 3px solid Highlight; outline-offset: -3px; }
  .pdf-file.is-over { outline: 2px solid Highlight; }
}

/* ---- 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 drop tile is the one thing on an empty page you are meant to act on, and
   it was --surface on --surface-2: on the white page that is a white square on a
   3%-grey square, the lowest-contrast object in the whole empty state. Same tint
   as the hero and card tiles, so one "this carries the mark" treatment covers
   all three places an icon appears. */
.pdf-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. */
.pdf-drop-icon svg { width: 30px; height: 30px; color: var(--accent); }
/* Tinted, not grey. /css/style.css draws the home page's card icons on an
   80px tile filled with rgba(accent,.15) and no hairline — that IS the site's
   card language. These were --surface-2 behind a --line hairline, so a wall of
   26 cards read as 26 grey chips, and in light mode the chip differed from the
   card under it by 3% luminance. --accent-soft is already in the file (the
   privacy note), so no new colour enters the palette; the hover border below
   still resolves to --accent, which now has a transparent border to grow from. */
.pdf-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; }
.pdf-card-icon svg { width: 21px; height: 21px; color: var(--accent); }
/* the card lifts on hover; the mark should follow it rather than sit still */
.pdf-card:hover .pdf-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 .pdf-hub. 1180px of `auto-fill, minmax(240px,1fr)`
   is four columns, and a wall with fewer cards than that cannot fill the row it
   was widened for — /tools/image/ has exactly three, so the widening bought it a
   285px hole in its only row and 240px of empty page each side of it. Eight is
   the smallest count that still fills two complete four-card rows. This suite's
   hub has fifteen and is unaffected; the rule is stated identically in img.css so
   the two files stay the same code.

   The live editors get the same measure for a different reason: a 900px column
   leaves the preview ~500px after the 340px control panel, so on a 1440 screen
   you judge a watermark, a crop or a signature on a page a third the size of the
   one you are editing.

   Gated on .pdf-editor-live, which every tool with a real preview column now
   carries — jpg-to-pdf included. It was excluded for a while, and the reason
   recorded for that was wrong twice over: its file list is #fileList, a
   full-width element ABOVE the editor, and its second column is a .pv-canvas
   previewing the page that will be created. Widening it genuinely did make the
   page worse at the time, because CSS can only cap a canvas with max-width and
   never grow it, so the column gained 280px of empty page around a picture its
   own script had pinned to 300px. That is fixed at the source: the markup gives
   that column a .pv-host panel and drawPreview() sizes to its stage the way
   pdf-preview.js always did, so the widening now has something to fill. */
@media (min-width: 1120px) {
  .pdf-main:has(.pdf-hub > :nth-child(8)),
  .pdf-main:has(.pdf-editor-live:not([hidden])) { max-width: 1180px; }
}
