/* Self-hosted so no font request ever leaves the browser. Only 400 and
   600 exist -- the only weights used anywhere on the site. */
@font-face{
  font-family:'IBM Plex Mono';
  font-style:normal;
  font-weight:400;
  font-display:swap;
  src:url('fonts/ibmplexmono-400.woff2') format('woff2');
}
@font-face{
  font-family:'IBM Plex Mono';
  font-style:normal;
  font-weight:600;
  font-display:swap;
  src:url('fonts/ibmplexmono-600.woff2') format('woff2');
}

:root{
  /* No dark theme built -- this tells the browser not to guess, so native
     form controls (select dropdowns, scrollbar on OSes where it draws one)
     render with light-appropriate defaults instead of possibly clashing
     dark ones under a dark OS/browser preference. */
  color-scheme:light;
  --bg:#FBF8FF;
  /* Warm-to-cool gradient used on every "flat white" surface (panels,
     cards, buttons), each at its own opacity. */
  --surface-warm-rgb:255,253,246;
  --surface-cool-rgb:248,246,255;
  --panel-bg:linear-gradient(135deg, rgba(255,253,246,0.82), rgba(248,246,255,0.82));
  --panel-bg-solid:linear-gradient(135deg, rgba(255,253,246,1), rgba(248,246,255,1));
  --border-color:#B79CFF;
  /* RGB of --border-color, for use inside rgba(). */
  --border-color-rgb:183,156,255;
  /* Shared ambient shadow for large panels (toolbar/preview-area/about-section). */
  --shadow-panel:0 8px 32px rgba(160,140,220,0.14);
  /* --text-color and --accent share --border-color's hue, just darker/
     more saturated, so text and borders read as one palette. */
  --text-color:#4824A8;
  /* Hue-matched to the pink nav-card's border, like mint/sky/peach's
     card+sticker pairs. */
  --pink-live:#FF6FD0;
  /* RGB of --pink-live, for rgba() washes. */
  --pink-live-rgb:255,111,208;
  --pink-live-dim:#D98FC9;
  /* Two-state pink used by toolbar buttons, the pink nav-card, and
     hover-state accents: soft at rest, vivid on hover. */
  --pink-soft:#FF9FE0;
  --pink-vivid:#FF38BF;
  /* Same saturation formula as --text-color and the tint text colors below. */
  --link-color:#9727B8;
  --blackout:#0A0C0E;
  /* Only colors reused in 2+ places get their own token here; single-use
     tint colors (mint's soft border, all of peach) stay plain hex. */
  --sky-soft:#7FD4FF;
  --sky-vivid:#20B4FF;
  --sky-text:#1B5D7E;
  --mint-vivid:#2DCF8A;
  --mint-text:#125438;
  --pink-text:#7E1B5E;
  /* RGB of --pink-text, for the dropzone's transparent drag-over wash. */
  --pink-text-rgb:126,27,94;
  --gradient-rainbow:linear-gradient(120deg,var(--pink-soft) 0%,var(--border-color) 30%,var(--sky-soft) 65%,#6BEFC2 100%);
  /* Colors buttons, cards, sliders, selects, and dropzones on hover --
     not just headings, despite the name. Tuned for 4.62:1 contrast
     against the page background. */
  --accent:#7D55E7;
  /* Matches --pink-soft, so every hard-offset text-shadow on the site
     uses the same pink. */
  --shadow-color:var(--pink-soft);
  /* The one hard-offset text-shadow recipe shared by every heading-style element. */
  --text-shadow:1px 1px 0 var(--shadow-color);
  /* --font-display and --font-body both alias --font-mono -- kept as
     separate names since call sites read as "heading" vs "body copy"
     even though the value is the same. */
  --font-mono:'IBM Plex Mono',monospace;
  --font-display:var(--font-mono);
  --font-body:var(--font-mono);
  /* rem, not px, so this respects the browser/OS text-size setting. */
  --font-size-base:0.9375rem;
  --font-size-small:0.875rem;
  /* One line-height used by every text element on the site. 1.6, up from
     the original 1.5. */
  --line-height:1.6;
  /* Shared shorthand recipes, so a value can't drift out of sync between call sites. */
  --border-default:2px solid var(--border-color);
  /* Shared column width so header/content/footer stay aligned. */
  --content-width:1200px;
  /* One border-radius for the whole site. */
  --radius:5px;
  --transition-hover:transform .1s ease, box-shadow .1s ease;
  --hover-lift:translate(-1px,-1px);
  --shadow-pop:3px 3px 0 rgba(var(--border-color-rgb),0.75);
  /* Same color as --shadow-pop, smaller offset for smaller elements like stickers. */
  --shadow-pop-sm:2px 2px 0 rgba(var(--border-color-rgb),0.75);
}
*{box-sizing:border-box;}
/* Hides the native scrollbar so the page can't reflow narrower when a
   scrollbar appears (that reflow was squishing the header's wavy edge).
   .scroll-indicator below replaces the lost scroll cue with a custom
   overlay that never occupies layout space. */
html{
  scrollbar-width:none; /* Firefox */
  -ms-overflow-style:none; /* legacy Edge/IE */
}
html::-webkit-scrollbar{ /* Chrome/Edge/Safari */
  width:0;
  height:0;
  background:transparent;
}
/* Custom scroll-position cue, replacing the hidden native scrollbar.
   Fixed positioning keeps it out of layout entirely. Height/top/
   visibility are driven by initScrollIndicator in shell.js. */
.scroll-indicator{
  position:fixed;
  top:0;
  right:3px;
  width:5px;
  border-radius:var(--radius);
  background:var(--border-color);
  opacity:0;
  transition:opacity .25s ease;
  pointer-events:none;
  z-index:1000;
}
.scroll-indicator.visible{opacity:1;}
@media (prefers-reduced-motion: reduce){
  .scroll-indicator{transition:none;}
}
html,body{margin:0;padding:0;}
body{
  background:
    radial-gradient(circle at 12% 18%, rgba(255,182,225,0.4), transparent 42%),
    radial-gradient(circle at 88% 12%, rgba(182,224,255,0.4), transparent 42%),
    radial-gradient(circle at 50% 85%, rgba(182,255,224,0.35), transparent 45%),
    radial-gradient(circle at 92% 92%, rgba(255,236,182,0.35), transparent 42%),
    var(--bg);
  background-attachment:fixed;
  color:var(--text-color);
  font-family:var(--font-body);
  font-weight:400;
  /* Plex Mono's default line-height is cramped at this size without this. */
  line-height:var(--line-height);
  min-height:100vh;
  /* Y2K lowercase look -- a display-only transform, so markup stays
     properly capitalized. */
  text-transform:lowercase;
}
/* Custom text-highlight, replacing the browser default. Reuses the
   pink-soft/pink-text pair from .mode-btn.active -- verified at
   5.1:1 contrast. */
::selection{background:var(--pink-soft);color:var(--pink-text);}
::-moz-selection{background:var(--pink-soft);color:var(--pink-text);}
a{color:var(--link-color);text-decoration:none;}
/* Browsers default <code> to their own generic monospace, not whatever
   font-family the surrounding text inherited -- without this it renders
   in a visibly different mono face than the rest of the page's Plex Mono. */
code{font-family:inherit;}
/* Form controls don't inherit text-transform by default, so without
   this they'd ignore the lowercase rule above. */
button, select, input{text-transform:inherit;}
a:hover, a:focus-visible{text-decoration:underline;}

/* Every focusable element reuses its own :hover treatment for
   :focus-visible (see each element's own :hover rule below), so
   keyboard focus looks the same as a mouse hover. outline:none removes
   the browser's default ring, which would otherwise double up. */
a:focus-visible,
.mode-btn:focus-visible,
.field select:focus-visible,
.field input[type=range]:focus-visible,
.video-controls input[type=range]:focus-visible,
.field input[type=number]:focus-visible,
.field input[type=text]:focus-visible,
.field input[type=color]:focus-visible,
.btn:focus-visible,
.dropzone:focus-visible,
input[type=checkbox]:focus-visible,
.nav-card:focus-visible{
  outline:none;
}

/* Inserted by Haven.renderHeader as the page's first focusable element.
   Off-screen until focused, so it's invisible to mouse users and doesn't
   affect layout, but lets keyboard/screen-reader users jump straight to
   <main> instead of tabbing through the header every page load. Animates
   transform only, not top/left, per the site's own animation guidelines. */
.skip-link{
  position:fixed;
  top:8px;
  left:8px;
  z-index:2000;
  background:var(--accent);
  color:#fff;
  padding:10px 16px;
  border-radius:var(--radius);
  border:var(--border-default);
  box-shadow:var(--shadow-pop);
  font-family:var(--font-mono);
  font-weight:600;
  text-decoration:none;
  transform:translateY(-80px);
  transition:transform .15s ease;
}
.skip-link:focus{
  transform:translateY(0);
  outline:none;
}
@media (prefers-reduced-motion: reduce){
  .skip-link{transition:none;}
}

header.shell-header{
  border-bottom:none;
  /* No backdrop-filter (blurs text on HiDPI) -- opacity alone gives the
     frosted-glass look. */
  background:linear-gradient(135deg, rgba(var(--surface-warm-rgb),0.88), rgba(var(--surface-cool-rgb),0.88));
  position:sticky;
  top:0;
  z-index:10;
  padding:20px 32px;
  display:flex;
  align-items:center;
  justify-content:space-between;
  flex-wrap:wrap;
  gap:12px;
}
/* Wavy edge, echoing the footer's .wavy-rule. A pseudo-element flush
   against the header's true bottom edge, so it doesn't affect the
   header's own padding or centering. */
header.shell-header::after{
  content:'';
  position:absolute;
  left:0;
  right:0;
  top:100%;
  height:14px;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='14' viewBox='0 0 28 14'%3E%3Cpath d='M0,7 Q7,0 14,7 T28,7' stroke='%23B79CFF' stroke-width='4' fill='none' stroke-linecap='round'/%3E%3C/svg%3E") top / 28px 14px repeat-x,
    linear-gradient(135deg, rgba(var(--surface-warm-rgb),0.88), rgba(var(--surface-cool-rgb),0.88));
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='14' viewBox='0 0 28 14'%3E%3Cpath d='M0,7 Q7,0 14,7 T28,7 L28,0 L0,0 Z' fill='white'/%3E%3C/svg%3E") top / 28px 14px repeat-x;
  mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='14' viewBox='0 0 28 14'%3E%3Cpath d='M0,7 Q7,0 14,7 T28,7 L28,0 L0,0 Z' fill='white'/%3E%3C/svg%3E") top / 28px 14px repeat-x;
  pointer-events:none;
}
/* baseline, not center -- keeps "haven" and the tagline sitting on
   the same visual line despite their very different font sizes. */
.wordmark{display:flex;align-items:baseline;gap:14px;}
.wordmark a{color:inherit;}
/* Beats the generic a:hover underline rule so the wordmark's own
   wavy-mask underline (below) doesn't get a second plain one stacked on it. */
.wordmark a:hover, .wordmark a:focus-visible{text-decoration:none;}
/* Underline is a mask, not text-decoration -- native text-decoration can't
   draw the wavy shape used site-wide (.footer-links a shares this same
   mask recipe), so this paints one flat wavy shape on the link's own box
   instead. Was also load-bearing for two-color text back when the
   wordmark read "loopback" (text-decoration-color isn't inherited by
   descendants) -- no longer strictly needed now that "haven" is a single
   color, but kept for the wavy-shape reason alone. */
.wordmark .site-name{
  font-family:var(--font-display);
  font-weight:600;
  font-size:1.25rem;
  line-height:var(--line-height);
  color:var(--accent);
  text-shadow:var(--text-shadow);
}
/* Terminal-prompt cursor -- a sharp on/off blink (step-end), not a smooth
   fade, so it reads as a blinking text cursor rather than a pulsing dot
   (see .readout .stat-sent::before for that other, deliberately different
   pulse). */
@keyframes terminal-blink{
  0%, 49%{opacity:1;}
  50%, 100%{opacity:0;}
}
/* Tightens the gap after the prompt chevron -- the glyph itself is a
   hollow shape, not a solid letterform, so a literal monospace space
   after it reads wider than the same space does between two letters.
   Pulls the following text in without touching the actual space
   character (which would either vanish it entirely or require a
   non-breaking-space hack). */
.wordmark .site-name-prompt{
  margin-right:-5px;
}
.wordmark .site-name-cursor{
  animation:terminal-blink 1s step-end infinite;
}
@media (prefers-reduced-motion: reduce){
  .wordmark .site-name-cursor{animation:none;}
}
.wordmark .tagline{
  font-family:var(--font-mono);
  font-size:var(--font-size-base);
  font-weight:600;
  color:var(--text-color);
}

.readout{
  font-family:var(--font-mono);
  font-size:var(--font-size-small);
  color:var(--text-color);
  text-align:right;
  line-height:var(--line-height);
  display:flex;
  flex-direction:column;
  gap:6px;
}
.readout .row{display:flex;gap:10px;justify-content:flex-end;align-items:center;}
.readout .label{color:var(--text-color);}
/* Solid tinted chip per stat, dark text shade of its own hue for contrast. */
.readout .stat-sent, .readout .stat-local, .stat-row .stat-value{
  display:inline-block;
  padding:1px 8px;
  border-radius:var(--radius);
  border-width:2px;
  border-style:solid;
  box-shadow:var(--shadow-pop-sm);
  font-variant-numeric:tabular-nums;
}
.readout .stat-sent{color:var(--pink-text);font-weight:600;background:linear-gradient(135deg, rgba(255,180,230,0.4), rgba(255,140,215,0.4));border-color:var(--pink-vivid);}
.readout .stat-sent::before{
  content:'';
  display:inline-block;
  width:6px;height:6px;
  border-radius:50%;
  background:var(--pink-live);
  margin-right:6px;
  vertical-align:1px;
  animation:pulse 2s ease-in-out infinite;
}
.readout .stat-local{color:var(--sky-text);background:linear-gradient(135deg, rgba(150,220,255,0.4), rgba(100,200,255,0.4));border-color:var(--sky-vivid);}
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:0.35;}}
@media (prefers-reduced-motion: reduce){
  .readout .stat-sent::before{animation:none;}
}

main{
  max-width:var(--content-width);
  margin:0 auto;
  /* padding-bottom:0 -- whatever follows </main> supplies its own top
     spacing (.wavy-rule or section.other-tools); a bottom padding here
     would double up with it. */
  padding:32px 32px 0;
}
main:not(.tool-main) > * + *{
  margin-top:28px;
}
main.tool-main{
  display:grid;
  grid-template-columns:240px 1fr;
  gap:28px;
  /* Same corrected value as .hero, and for the same reason -- see .hero's
     own comment below for how the true 28px gap was measured. */
  margin-top:6.45px;
}
/* Grid items default to a content-based min-width, so a long filename
   in the merge file list could force this column (and the page) wider
   than the viewport without this. */
main.tool-main > .toolbar, main.tool-main > .preview-area{
  min-width:0;
}
main.tool-main .about-section{
  grid-column:1 / -1;
}
@media (max-width:800px){
  main.tool-main{grid-template-columns:1fr;}
  /* Stacked single-column, .toolbar (controls) would otherwise land above
     .preview-area (the dropzone) just because it's first in the HTML --
     fine side-by-side on desktop, but on mobile it buries the actual
     "drop your image here" prompt below a wall of controls that are all
     disabled until a file is loaded. order swaps the visual stack without
     touching the DOM or desktop's grid-template-columns layout above. */
  main.tool-main > .preview-area{order:-1;}
  main.tool-main > .toolbar{order:0;}
  /* Stack wordmark and readout as full-width rows instead of sharing
     one -- prevents the tagline from being squeezed into a tall stack
     of short wrapped lines. */
  header.shell-header{
    flex-direction:column;
    align-items:stretch;
    gap:10px;
  }
  .wordmark{
    flex-direction:column;
    align-items:flex-start;
    gap:2px;
  }
}

.tool-title{
  font-family:var(--font-display);
  font-weight:600;
  font-size:1.25rem;
  line-height:var(--line-height);
  color:var(--accent);
  margin:0;
  text-align:center;
  text-shadow:var(--text-shadow);
  text-wrap:balance;
}

.toolbar{
  background:var(--panel-bg);
  border:var(--border-default);
  border-radius:var(--radius);
  box-shadow:var(--shadow-panel);
  padding:20px;
  display:flex;
  flex-direction:column;
  gap:20px;
}
/* fieldset/legend for choice-button groups (Effect/Shape in redact,
   Direction/Border in merge) -- the correct pairing for a labeled
   group of mutually-exclusive controls. min-width:0 works around
   fieldset's default min-width:min-content in flex/grid layouts
   (unlike a plain div, whose default is 0). */
fieldset{border:none;margin:0;padding:0;min-width:0;}
.toolbar h2, .toolbar legend{
  font-family:var(--font-mono);
  font-weight:600;
  font-size:var(--font-size-small);
  color:var(--text-color);
  margin:0;
  padding:0;
}
.mode-btn{
  display:flex;
  align-items:center;
  gap:10px;
  background:linear-gradient(135deg, rgba(var(--surface-warm-rgb),0.5), rgba(var(--surface-cool-rgb),0.5));
  border:var(--border-default);
  color:var(--text-color);
  font-family:var(--font-body);
  font-weight:400;
  font-size:var(--font-size-small);
  padding:10px 12px;
  border-radius:var(--radius);
  cursor:pointer;
  text-align:left;
  width:100%;
  transition:var(--transition-hover);
}
.mode-btn:hover:not(:disabled), .mode-btn:focus-visible:not(:disabled){
  border-color:var(--accent);
  transform:var(--hover-lift);
  box-shadow:var(--shadow-pop);
}
/* Same muted look as every other disabled control -- was missing here since
   no mode-btn group had ever actually been disabled before Watermark's
   Placement fieldset (mode selection elsewhere, like redact's Effect/Shape
   or Watermark's own Text/Image, stays choosable pre-content). */
.mode-btn:disabled{opacity:0.4;cursor:not-allowed;}
/* Matches the pink nav-card's own soft/vivid two-state border, not the
   single fixed --pink-live color. */
.mode-btn.active{border-color:var(--pink-soft);background:rgba(var(--pink-live-rgb),0.16);color:var(--pink-text);}
.mode-btn.active:hover:not(:disabled), .mode-btn.active:focus-visible:not(:disabled){border-color:var(--pink-vivid);}
/* Shared small icon for mode-btn rows (shape/direction pickers). */
.shape-icon{width:14px;height:14px;flex-shrink:0;display:flex;align-items:center;justify-content:center;}
.shape-icon svg{width:100%;height:100%;}
/* Shared row of icon-only buttons for undo/redo -- was duplicated
   identically in redact's and transform's local <style> blocks. */
.history-row{display:flex;gap:8px;}
.history-row .btn{flex:1;}
.history-row .shape-icon{width:18px;height:18px;margin:0 auto;}
/* Shared choice-button list (Effect/Shape/Direction/Border/Crop-ratio) --
   was an identical inline style repeated on 5 separate elements across 3
   tool pages, at gap:6px, while .actions/.history-row (the same kind of
   button stack, conceptually) used gap:8px -- same pattern, inconsistent
   size. Unified to 8px here instead of guessing which value to keep. */
.modes{margin-top:10px;display:flex;flex-direction:column;gap:8px;}
/* Shared 3x3 quick-position grid -- was transform-local (`.crop-position-grid`)
   until Watermark needed an identical 9-anchor placement picker. 1fr columns
   stretch full-width like every other button group on the page; content-based
   padding (not aspect-ratio/a fixed size) keeps the height modest while the
   width still stretches to fill -- see CLAUDE.md for why the two aren't both
   achievable at once here. */
.position-grid{
  display:grid;grid-template-columns:repeat(3,1fr);gap:8px;
  margin-top:8px;
}
.position-grid .btn{padding:8px;display:flex;align-items:center;justify-content:center;}
/* Only needed by Watermark (position is a persisted choice that stays
   selected and drives the live preview) -- Transform's own position-grid
   use is a one-shot placement action with nothing to keep "selected", so
   it never sets this class. Same active look as .mode-btn.active. */
.position-grid .btn.active{border-color:var(--pink-soft);background:rgba(var(--pink-live-rgb),0.16);color:var(--pink-text);}
.position-grid .btn.active:hover, .position-grid .btn.active:focus-visible{border-color:var(--pink-vivid);}
/* Shared by Image/Video Transform's Crop fieldset -- .ratio-row pairs a
   ratio preset with its own portrait-orientation toggle button; .lock-row
   is the Resize fieldset's lock-aspect-ratio checkbox row. Was duplicated
   identically in both tools' local <style> blocks. */
.lock-row{
  display:flex;align-items:center;gap:6px;
  font-family:var(--font-mono);font-size:var(--font-size-small);color:var(--text-color);
  margin-top:6px;
  /* Text alone renders this row at 22px tall, under WCAG 2.5.8's 24px
     touch-target minimum -- the checkbox's own real hit target is this
     whole label (see the lock-row markup), not just the 16px box. */
  min-height:24px;
}
.ratio-row{display:flex;gap:8px;}
.ratio-row .mode-btn{flex:1;}
.ratio-row .orientation-btn{flex-shrink:0;width:36px;padding:0;display:flex;align-items:center;justify-content:center;}

.field{display:flex;flex-direction:column;gap:6px;}
.field label{
  font-family:var(--font-mono);
  font-size:var(--font-size-small);
  color:var(--text-color);
  display:flex;
  justify-content:space-between;
}
.field input[type=range], .video-controls input[type=range]{
  -webkit-appearance:none;
  appearance:none;
  width:100%;
  height:2px;
  border-radius:999px;
  background:var(--border-color);
  cursor:pointer;
}
/* Was missing entirely -- a fully custom-styled range input (track/thumb
   both rebuilt from scratch above) doesn't get any native disabled look for
   free, so without this a disabled slider looked identical to a working
   one. Same recipe as every other disabled field on the site. */
.field input[type=range]:disabled, .video-controls input[type=range]:disabled{opacity:0.4;cursor:not-allowed;}
.field input[type=range]::-webkit-slider-runnable-track, .video-controls input[type=range]::-webkit-slider-runnable-track{
  height:2px;
  border-radius:999px;
  background:var(--border-color);
}
.field input[type=range]::-moz-range-track, .video-controls input[type=range]::-moz-range-track{
  height:2px;
  border-radius:999px;
  background:var(--border-color);
}
/* Same two-state pink as the toolbar buttons: soft at rest, vivid on hover. */
.field input[type=range]::-webkit-slider-thumb, .video-controls input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;
  appearance:none;
  width:16px;
  height:16px;
  border-radius:var(--radius);
  background:rgba(var(--pink-live-rgb),0.16);
  border:2px solid var(--pink-soft);
  margin-top:-7px;
  cursor:pointer;
}
.field input[type=range]::-moz-range-thumb, .video-controls input[type=range]::-moz-range-thumb{
  width:16px;
  height:16px;
  border-radius:var(--radius);
  background:rgba(var(--pink-live-rgb),0.16);
  border:2px solid var(--pink-soft);
  cursor:pointer;
}
.field input[type=range]:hover:not(:disabled)::-webkit-slider-thumb,
.field input[type=range]:focus-visible:not(:disabled)::-webkit-slider-thumb,
.video-controls input[type=range]:hover:not(:disabled)::-webkit-slider-thumb,
.video-controls input[type=range]:focus-visible:not(:disabled)::-webkit-slider-thumb{border-color:var(--pink-vivid);}
.field input[type=range]:hover:not(:disabled)::-moz-range-thumb,
.field input[type=range]:focus-visible:not(:disabled)::-moz-range-thumb,
.video-controls input[type=range]:hover:not(:disabled)::-moz-range-thumb,
.video-controls input[type=range]:focus-visible:not(:disabled)::-moz-range-thumb{border-color:var(--pink-vivid);}
.field select{
  background:var(--panel-bg-solid);
  color:var(--text-color);
  border:var(--border-default);
  border-radius:var(--radius);
  padding:10px 12px;
  font-family:var(--font-body);
  font-weight:400;
  font-size:var(--font-size-small);
  accent-color:var(--text-color);
  transition:var(--transition-hover);
}
/* Same hover recipe as .btn/.mode-btn. */
.field select:hover:not(:disabled), .field select:focus-visible:not(:disabled){
  border-color:var(--accent);
  transform:var(--hover-lift);
  box-shadow:var(--shadow-pop);
}
/* Same muted look as a disabled button -- was missing, so a disabled
   field looked identical to an enabled one with nothing to signal why
   clicking/typing did nothing. */
.field select:disabled{opacity:0.4;cursor:not-allowed;}
/* Best-effort only -- the open <select> popup's highlight color is
   OS-drawn chrome that CSS can't fully override. */
.field select option{
  background:var(--panel-bg-solid);
  color:var(--text-color);
}
/* Same recipe as .field select -- was missing entirely, so number inputs
   fell back to the browser's raw default border/focus ring (a jarring
   orange outline next to this site's purple palette). Shared with
   type=text (Watermark's text field) since both are plain boxed text
   entry -- only the number-specific bits (spin buttons, textfield
   appearance) stay scoped to type=number alone, below. */
.field input[type=number], .field input[type=text]{
  background:var(--panel-bg-solid);
  color:var(--text-color);
  border:var(--border-default);
  border-radius:var(--radius);
  padding:10px 12px;
  font-family:var(--font-body);
  font-weight:400;
  font-size:var(--font-size-small);
  transition:var(--transition-hover);
  /* Was implicitly full-width before via .field's own flex-stretch (the
     input used to be .field's direct child). Now that .stepper-input
     (a plain, non-flex div) sits in between, the input reverts to its own
     intrinsic default width unless told otherwise -- which overflowed
     past the narrow 240px toolbar column on desktop once flanked by two
     36px stepper buttons. */
  width:100%;
  min-width:0;
}
.field input[type=number]{
  -moz-appearance:textfield;
  appearance:textfield;
}
.field input[type=number]:hover:not(:disabled), .field input[type=number]:focus-visible:not(:disabled),
.field input[type=text]:hover:not(:disabled), .field input[type=text]:focus-visible:not(:disabled){
  border-color:var(--accent);
  transform:var(--hover-lift);
  box-shadow:var(--shadow-pop);
}
/* Same muted look as a disabled button -- was missing, so a disabled
   field looked identical to an enabled one with nothing to signal why
   clicking/typing did nothing. */
.field input[type=number]:disabled, .field input[type=text]:disabled{opacity:0.4;cursor:not-allowed;}
/* The Y2K lowercase look (body, above) is display-only for the site's own
   copy -- a typed value here is the user's actual creative content
   (Watermark's text field), so it stays in whatever case they typed
   instead of silently rendering different from what gets stamped onto
   the image. The placeholder is still our own copy, so it keeps the
   lowercase look (set below, after this reset). */
.field input[type=text]{text-transform:none;}
.field input[type=text]::placeholder{text-transform:lowercase;}
/* Same purple as every border on the site, at full opacity -- not the
   browser's default plain grey placeholder text. --text-color at reduced
   opacity (the first thing tried here) read as too washed-out/light. */
.field input[type=number]::placeholder, .field input[type=text]::placeholder{
  color:var(--border-color);
}
/* Hides the native up/down spin buttons -- their arrow glyph is
   OS-drawn chrome that CSS can't recolor to match the site palette (only
   .field select's own OS-drawn dropdown highlight has this same limit).
   Typing a value directly, or the keyboard's own up/down arrow keys,
   both still work with the spinner hidden. */
.field input[type=number]::-webkit-inner-spin-button,
.field input[type=number]::-webkit-outer-spin-button{
  -webkit-appearance:none;
  appearance:none;
  margin:0;
}
/* Native color swatch, just given the same border/radius/hover treatment
   as every other field -- unlike the checkbox/spin-button/select-popup
   cases above, the swatch square itself doesn't need to be hidden and
   rebuilt, since its only "OS chrome" is the color it displays, which is
   already whatever the user picked. */
.field input[type=color]{
  width:100%;
  height:42px;
  padding:4px;
  border:var(--border-default);
  border-radius:var(--radius);
  background:var(--panel-bg-solid);
  cursor:pointer;
  transition:var(--transition-hover);
}
.field input[type=color]:hover:not(:disabled), .field input[type=color]:focus-visible:not(:disabled){
  border-color:var(--accent);
  transform:var(--hover-lift);
  box-shadow:var(--shadow-pop);
}
.field input[type=color]:disabled{opacity:0.4;cursor:not-allowed;}
.field input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
.field input[type=color]::-webkit-color-swatch{border:none;border-radius:calc(var(--radius) - 2px);}
.field input[type=color]::-moz-color-swatch{border:none;border-radius:calc(var(--radius) - 2px);}
/* "px" (or any unit) painted on top of a number input, not typed into its
   value -- type=number can't hold non-numeric characters, so this is the
   only way to show a unit without switching to a plain text input and
   losing numeric validation/keyboard behavior. Extra padding-right on the
   input leaves room so the suffix never overlaps a typed number. */
.field-unit{position:relative;}
.field-unit input[type=number]{padding-right:34px;}
.field-unit .unit-suffix{
  position:absolute;
  right:12px;
  top:50%;
  transform:translateY(-50%);
  font-family:var(--font-mono);
  font-size:var(--font-size-small);
  color:var(--text-color);
  pointer-events:none;
}
/* The suffix is a sibling, not part of the input itself, so :disabled's
   opacity on the input doesn't reach it automatically -- needs its own
   rule to grey out at the same time. */
.field-unit input[type=number]:disabled + .unit-suffix{opacity:0.4;}
/* Custom +/- buttons standing in for the native spin arrows hidden above --
   same "OS-drawn chrome can't be recolored, so hide it and rebuild it
   ourselves" move already used for the checkbox. Bigger and easier to hit
   than the native spinner's tiny arrows, especially with a finger on
   mobile. .stepper-input takes over .field-unit's role (relative
   positioning + padding for the "px" suffix) since the outer .field is now
   the 3-wide flex row instead of the direct input/suffix wrapper. */
.stepper{display:flex;gap:8px;}
.stepper-input{position:relative;flex:1;min-width:0;}
.stepper-btn{
  flex-shrink:0;
  width:36px;
  padding:0;
  display:flex;
  align-items:center;
  justify-content:center;
  font-size:1.1rem;
  line-height:1;
}
/* Custom box since the native checkbox can't be restyled directly (only
   hidden and rebuilt). Checked state reuses the same pink used by
   .mode-btn.active; the checkmark is a rotated border corner, not an image. */
input[type=checkbox]{
  appearance:none;
  -webkit-appearance:none;
  width:16px;
  height:16px;
  flex-shrink:0;
  margin:0;
  border:var(--border-default);
  border-radius:calc(var(--radius) - 2px);
  background:var(--panel-bg-solid);
  cursor:pointer;
  position:relative;
  transition:var(--transition-hover);
}
input[type=checkbox]:hover, input[type=checkbox]:focus-visible{
  border-color:var(--accent);
  transform:var(--hover-lift);
  box-shadow:var(--shadow-pop);
}
input[type=checkbox]:checked{
  border-color:var(--pink-soft);
  background:rgba(var(--pink-live-rgb),0.16);
}
input[type=checkbox]:checked:hover, input[type=checkbox]:checked:focus-visible{
  border-color:var(--pink-vivid);
}
input[type=checkbox]:checked::after{
  content:'';
  position:absolute;
  left:4px;
  top:0px;
  width:4px;
  height:8px;
  border:solid var(--pink-text);
  border-width:0 2px 2px 0;
  transform:rotate(45deg);
}
/* Windows Forced Colors Mode flattens most author background/border colors
   to system ones, which would otherwise make checked and unchecked read as
   the same box (the pink background/border difference above disappears).
   This doesn't opt out of forced-colors (that's the actual accessibility
   anti-pattern, via forced-color-adjust:none) -- it adapts to it, using the
   same Highlight/HighlightText system-color keywords native checkboxes
   themselves use to mark a selected state under this mode. */
@media (forced-colors: active){
  input[type=checkbox]:checked{
    background:Highlight;
    border-color:Highlight;
  }
  input[type=checkbox]:checked::after{
    border-color:HighlightText;
  }
}

.actions{display:flex;flex-direction:column;gap:8px;}
.btn{
  font-family:var(--font-body);
  font-size:var(--font-size-small);
  font-weight:400;
  padding:10px 12px;
  border-radius:var(--radius);
  border:var(--border-default);
  background:linear-gradient(135deg, rgba(var(--surface-warm-rgb),0.6), rgba(var(--surface-cool-rgb),0.6));
  color:var(--text-color);
  cursor:pointer;
  transition:var(--transition-hover);
}
.btn:hover:not(:disabled), .btn:focus-visible:not(:disabled){
  border-color:var(--accent);
  transform:var(--hover-lift);
  box-shadow:var(--shadow-pop);
}
.btn:disabled{opacity:0.4;cursor:not-allowed;}
.btn.primary{
  background:var(--gradient-rainbow);
  color:var(--text-color);
  border:var(--border-default);
}
.btn.primary:hover:not(:disabled), .btn.primary:focus-visible:not(:disabled){transform:var(--hover-lift);box-shadow:var(--shadow-pop);}
.btn.primary:disabled{box-shadow:none;}

.preview-area{
  background:var(--panel-bg);
  border:var(--border-default);
  border-radius:var(--radius);
  box-shadow:var(--shadow-panel);
  padding:20px;
  display:flex;
  /* center, not flex-start -- this is a grid item that often has extra
     vertical space next to a small image; this uses it instead of
     leaving it all below a top-pinned image. */
  align-items:center;
  justify-content:center;
  gap:28px;
  flex-wrap:wrap;
  position:relative;
}
.dropzone{
  width:100%;
  flex:1 1 auto;
  min-width:0;
  align-self:stretch;
  min-height:180px;
  border:2px dashed var(--border-color);
  border-radius:var(--radius);
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:10px;
  color:var(--text-color);
  cursor:pointer;
  text-align:center;
  padding:20px;
  transition:var(--transition-hover);
}
/* Placed before .dropzone.dragover below so the drag-in-progress
   border color still wins when both apply at once. */
.dropzone:hover, .dropzone:focus-visible{
  border-color:var(--accent);
  transform:var(--hover-lift);
  box-shadow:var(--shadow-pop);
}
/* Matches the redact card's pink, but more transparent so the drop
   target reads as a light overlay, not a solid pink card. */
.dropzone.dragover{
  border-color:var(--pink-soft);
  color:rgba(var(--pink-text-rgb),0.7);
  background:linear-gradient(135deg, rgba(255,180,230,0.12), rgba(255,140,215,0.12));
}
/* .drop-title sets its own explicit color, so it needs this override too. */
.dropzone.dragover .drop-title{color:rgba(var(--pink-text-rgb),0.7);}
.dropzone .hint{font-family:var(--font-mono);font-size:var(--font-size-small);}
.dropzone .drop-title{font-family:var(--font-display);font-weight:600;font-size:var(--font-size-base);line-height:var(--line-height);color:var(--text-color);}
input[type=file]{display:none;}

#canvasWrap{display:none;max-width:100%;line-height:0;}
/* text-align:center -- canvasStack is inline-block, so a wide
   canvasWrap would otherwise leave a small canvas left-aligned inside it. */
#canvasWrap.active{display:block;flex:1 1 420px;min-width:0;max-width:600px;margin:0 auto;text-align:center;}
/* Opt-in override for tools with no side result/stats panel next to the
   canvas (redact, adjust, watermark) -- without a panel competing for
   space, the 600px cap above just left a large, pointless gap around a
   large source image. Tools that DO have a side panel (compress, convert,
   transform) keep the cap so the panel has room. */
#canvasWrap.active.wide{max-width:100%;flex:1 1 auto;}
/* margin:auto, not text-align -- canvas is a block-level replaced
   element, so text-align on an ancestor has no effect on it. */
canvas{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:var(--radius);}
/* Shared by every tool with an interactive canvas overlay (redact's brush
   preview, transform's crop selection) -- was duplicated identically in
   both pages' local <style> blocks before this. */
#canvasStack{position:relative;display:inline-block;line-height:0;}
#overlay{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;border-radius:var(--radius);}
.hint-text{font-family:var(--font-mono);font-size:var(--font-size-small);color:var(--text-color);margin:0;}

.result-panel{width:100%;display:none;flex-direction:column;gap:16px;}
/* flex-shrink:1 + max-width, not a fixed width -- lets this shrink on
   narrow phones instead of overflowing .preview-area's padding. */
.result-panel.active{display:flex;flex:1 1 240px;max-width:240px;}
.stat-row{
  display:flex;
  align-items:center;
  justify-content:space-between;
  font-family:var(--font-mono);
  font-size:var(--font-size-small);
  border-bottom:1px solid var(--border-color);
  padding-bottom:10px;
}
.stat-row .stat-value{color:#361B7E;background:linear-gradient(135deg, rgba(200,180,255,0.3), rgba(165,140,255,0.3));border-color:var(--accent);}
.stat-row .stat-value.good{color:var(--mint-text);background:linear-gradient(135deg, rgba(140,230,190,0.4), rgba(90,215,160,0.4));border-color:var(--mint-vivid);}
/* ul/li, not div -- a genuine reorderable list. Margin is reset on all
   sides (not just top) since <ul>'s UA default margin:1em 0 showed as
   a gap under "Images (0)" once the list was empty. */
.file-list{
  display:flex;
  flex-direction:column;
  gap:6px;
  max-height:280px;
  overflow-y:auto;
  margin:10px 0 0;
  padding:0;
  list-style:none;
}
/* An empty list shouldn't add its own margin on top of the action row's. */
.file-list:empty{margin-top:0;}
.file-row{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:8px;
  border:var(--border-default);
  border-radius:var(--radius);
  padding:8px 12px;
  font-size:var(--font-size-base);
}
/* text-transform:none for the same reason as the text input above -- an
   actual uploaded file name, not our own copy, so it stays as the user's
   real file name instead of looking silently altered. */
.file-row .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1;text-transform:none;}
/* Only needed by Watermark's image list (each row is a selectable "which
   image am I editing" choice, unlike Merge's file-row which has no concept
   of a currently-active item) -- same active look as .mode-btn.active. */
.file-row.active{border-color:var(--pink-soft);background:rgba(var(--pink-live-rgb),0.16);}

/* The up/down/remove icon buttons in a .file-row (image/video/audio Merge) --
   was duplicated identically across all three tools' local <style> blocks.
   24px, not the original 22px -- WCAG 2.5.8's touch-target minimum. */
.thumb-btn{
  width:24px;height:24px;
  border:var(--border-default);
  border-radius:var(--radius);
  background:rgba(255,255,255,0.5);
  color:var(--text-color);
  cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  padding:0;
}
.thumb-btn:not(:disabled):hover, .thumb-btn:not(:disabled):focus-visible{border-color:var(--pink-live-dim);color:var(--link-color);}
.thumb-btn:focus-visible{outline:none;}
.thumb-btn:disabled{opacity:0.3;cursor:not-allowed;}

/* A .file-row's own preview image (image Merge's source thumbnail, video
   Merge's grabbed frame). */
.thumb{width:28px;height:28px;border-radius:var(--radius);object-fit:cover;flex-shrink:0;border:var(--border-default);}

/* Custom playback bar shared by every video tool whose live preview has to
   stay canvas-based (crop-drag, rotate/flip, watermark stamping all need
   the exact pixels a real <video controls> element won't let scripts
   redraw) -- a genuine native video player isn't achievable for those
   without either breaking the interaction or risking the preview no longer
   matching the exported file, so this is the one playback UI reused across
   Video Adjust/Transform/Watermark instead of each tool's own bare
   <input type=range> scrubber. */
.video-controls{
  display:none;
  align-items:center;
  gap:10px;
  width:100%;
  margin-top:10px;
  padding:8px 12px;
  border:var(--border-default);
  border-radius:var(--radius);
  background:var(--panel-bg-solid);
}
.video-controls.active{display:flex;}
.video-controls .control-btn{
  flex-shrink:0;
  width:32px;height:32px;
  border:var(--border-default);
  border-radius:var(--radius);
  background:rgba(255,255,255,0.5);
  color:var(--text-color);
  cursor:pointer;
  display:flex;align-items:center;justify-content:center;
  padding:0;
  transition:var(--transition-hover);
}
.video-controls .control-btn:hover, .video-controls .control-btn:focus-visible{border-color:var(--accent);color:var(--link-color);}
.video-controls .control-btn:focus-visible{outline:none;}
.video-controls .video-time{font-family:var(--font-mono);font-size:var(--font-size-small);color:var(--text-color);white-space:nowrap;flex-shrink:0;font-variant-numeric:tabular-nums;}
/* flex-basis (not width) used here on purpose -- the shared
   ".video-controls input[type=range]{width:100%}" rule above is
   accidentally MORE specific than a plain ".video-controls .volume-slider"
   class selector ([type=range] counts as a specificity point same as a
   class, so it's 0,2,1 vs 0,2,0), so width:70px there would silently lose
   the cascade and the slider would inherit 100% width. flex-basis sizes
   the item directly and wins over width regardless of which one the
   cascade picks. */
.video-controls input[type=range]:not(.volume-slider){flex:1 1 0%;min-width:0;}
.video-controls .volume-slider{flex:0 0 70px;}
/* On a narrow phone, the two 32px buttons + time label + 70px volume
   slider leave no room at all for the scrubber (it was measured
   collapsing to 0px width, fully unusable, on a 375px viewport) --
   hiding the volume slider frees enough space to keep it draggable.
   Mute stays reachable via the mute button; phones already have a
   hardware volume control. */
@media (max-width:480px){
  .video-controls .volume-slider{display:none;}
}

/* Hides the raw decode-only source video for tools whose live preview
   draws through a canvas instead (Adjust/Transform/Watermark/Merge) --
   was duplicated identically across each one's own local <style> block. */
#sourceVideo{display:none;}
/* The toggleable bordered preview box shown once a file loads -- shared
   by every canvas-preview video tool, plus Merge's own <video> preview
   (#previewVideo), which used the identical recipe a second time. */
#previewCanvas, #previewVideo{
  display:none;
  max-width:100%;
  border:var(--border-default);
  border-radius:var(--radius);
}
#previewCanvas.active, #previewVideo.active{display:block;}
/* Shared start/end or current/duration time display -- was duplicated
   across Audio Edit, Video Trim, and Video GIF's own local <style>
   blocks (Audio Edit's copy was missing tabular-nums, unlike the other
   two -- fixed by consolidating on the more complete version). */
.time-readout{
  display:flex;
  justify-content:space-between;
  font-size:var(--font-size-small);
  color:var(--text-color);
  margin-top:8px;
  font-variant-numeric:tabular-nums;
}

/* ---------- FAQ (native <details>/<summary>, no JS needed) ---------- */
.faq-item{border-top:1px solid rgba(var(--border-color-rgb),0.25);padding:14px 0;}
.faq-item:first-of-type{border-top:none;padding-top:0;}
.faq-item summary{
  cursor:pointer;list-style:none;
  font-family:var(--font-mono);font-weight:600;
  font-size:var(--font-size-base);color:var(--text-color);
  display:flex;align-items:baseline;gap:10px;
}
.faq-item summary::-webkit-details-marker{display:none;}
.faq-item summary::before{content:'+';color:var(--accent);font-weight:600;flex-shrink:0;}
.faq-item[open] summary::before{content:'\2212';}
.faq-item summary:focus-visible{outline:3px solid var(--accent);outline-offset:2px;}
.faq-item p{margin:10px 0 0;font-size:var(--font-size-small);color:var(--text-color);}

/* Tiled SVG wave divider -- border-style has no "wavy" option. Color is
   hardcoded to match --border-color, since custom properties can't be
   read inside a data-URI SVG. */
.wavy-rule{
  height:12px;
  width:240px;
  /* 26px, not 28px -- the visible stroke sits ~2px inset from the tile
     edge, so this nets out to the same true 28px gap used everywhere else. */
  margin:26px auto;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='12' viewBox='0 0 28 12'%3E%3Cpath d='M0 6 Q7 0 14 6 T28 6' stroke='%23B79CFF' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat:repeat-x;
}
/* Zeroed out since .other-tools already carries its own bottom
   padding -- avoids doubling the gap. */
section.other-tools + .wavy-rule{margin:0 auto 18px;}

section.other-tools{
  max-width:var(--content-width);
  margin:28px auto 0;
  /* This padding is the entire gap to the wave below (wavy-rule and
     site-footer both have margin-top:0), so it matches the 28px rhythm
     used everywhere else on the page. */
  padding:0 32px 26px;
}
footer.site-footer{
  max-width:var(--content-width);
  margin:0 auto;
  padding:0 32px 40px;
}
/* Scoped to the tool-page footer chain only -- homepage/legal pages
   already get their full 28px from .wavy-rule's own margin. */
section.other-tools + .wavy-rule + footer.site-footer{padding-top:8px;}
.footer-links{
  display:flex;
  gap:20px;
  justify-content:center;
  font-family:var(--font-mono);
  font-size:var(--font-size-small);
  color:var(--text-color);
}
/* Shared wavy-underline recipe for every link that gets one. Sized to
   clear descenders (p/y/g) consistently regardless of the link's own font-size. */
.footer-links a, .wordmark a{
  position:relative;
}
/* Text alone renders these at 22px tall (14px font * 1.6 line-height),
   under WCAG 2.5.8's 24px touch-target minimum -- padding grows the real
   hit area, and the matching negative margin cancels the flow impact so
   the footer row's own height/spacing doesn't change. bottom on the
   underline pseudo below is adjusted by the same 2px so it still sits
   right under the text instead of drifting down with the taller box. */
.footer-links a{
  padding:2px 0;
  margin:-2px 0;
}
.footer-links a::before, .footer-links a::after,
.wordmark a::before, .wordmark a::after{
  content:'';
  position:absolute;
  left:0;
  right:0;
  bottom:-4px;
  height:5px;
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='7' viewBox='0 0 14 7'%3E%3Cpath d='M0 3.5 Q3.5 0 7 3.5 T14 3.5' stroke='white' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E") left / 14px 7px repeat-x;
  mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='7' viewBox='0 0 14 7'%3E%3Cpath d='M0 3.5 Q3.5 0 7 3.5 T14 3.5' stroke='white' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E") left / 14px 7px repeat-x;
}
/* Two mask layers, not filter:drop-shadow -- filter runs before
   masking, so a drop-shadow would shadow the pre-mask rectangle, not
   the wavy shape. ::before is a second copy of the mask, colored
   --shadow-color and nudged 1px/1px behind ::after, the same
   hard-offset-shadow trick used everywhere else on the site. */
/* Compensates for the 2px bottom padding added above -- without this the
   underline would sit 2px lower than the text now that the link's own
   padding-box bottom edge moved down. */
.footer-links a::before, .footer-links a::after{bottom:-2px;}
.footer-links a::after{background:var(--border-color);}
.footer-links a::before,
.wordmark a::before, .wordmark a::after{display:none;}
.footer-links a:hover::after, .footer-links a:focus-visible::after,
.wordmark a:hover::after, .wordmark a:focus-visible::after{
  display:block;
  /* Matches the toolbar buttons' vivid hover pink, not the general --pink-live accent. */
  background:var(--pink-vivid);
}
.footer-links a:hover::before, .footer-links a:focus-visible::before,
.wordmark a:hover::before, .wordmark a:focus-visible::before{
  display:block;
  background:var(--shadow-color);
  transform:translate(1px,1px);
}
.footer-links a{color:var(--text-color);}
/* Beats the generic underline rule so it doesn't double up with the
   wavy mask underline above. */
.footer-links a:hover, .footer-links a:focus-visible{
  color:var(--accent);
  text-decoration:none;
  text-shadow:var(--text-shadow);
}
/* Whichever footer link matches the current page (aria-current="page",
   set in renderSiteFooter) gets the same treatment hover already uses,
   permanently -- so it's clear which of the three you're already on. */
.footer-links a[aria-current="page"]{
  color:var(--accent);
  text-shadow:var(--text-shadow);
}
.footer-links a[aria-current="page"]::after{
  display:block;
  background:var(--pink-vivid);
}
.copyright{
  font-family:var(--font-mono);
  font-size:var(--font-size-base);
  font-weight:600;
  color:var(--accent);
  text-shadow:var(--text-shadow);
  text-align:center;
  margin:0 0 4px;
}
.other-tools-label{
  font-family:var(--font-mono);
  font-size:var(--font-size-base);
  font-weight:600;
  color:var(--accent);
  text-shadow:var(--text-shadow);
  margin:0 0 12px;
  text-align:center;
}
/* Homepage-only: groups the tool grid into Image/Audio/Video sections.
   #tool-grid itself is a direct main child (gets the standard 28px rhythm
   from main's own >*+* rule) -- this is just the gap BETWEEN category
   sections stacked inside it, same 28px so a 2nd/3rd category reads as a
   new section, not a cramped continuation of the first. */
.tool-category + .tool-category{margin-top:28px;}
/* 175px, not 200px -- at --content-width's 1200px, 200px only fit 5 per
   row, pushing a 6th/7th card (the homepage's Transform card, or any tool
   page's 6-card "other tools" row) onto its own row by itself. 175px is
   the largest min-width that still fits 6 across at this container width. */
.nav-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(175px,1fr));
  gap:12px;
}
.nav-card{
  display:flex;
  flex-direction:column;
  justify-content:center;
  border:var(--border-default);
  background:linear-gradient(135deg, rgba(var(--surface-warm-rgb),0.75), rgba(var(--surface-cool-rgb),0.75));
  border-radius:var(--radius);
  padding:16px;
  color:inherit;
  transition:var(--transition-hover);
}
.nav-card:hover, .nav-card:focus-visible{
  border-color:var(--accent);
  box-shadow:var(--shadow-pop);
  transform:var(--hover-lift);
  text-decoration:none;
}
/* Each card's border/hover matches its own tint hue instead of one
   uniform purple, so the nav grid doesn't read as purple on purple on purple. */
.nav-card.tint-pink{background:linear-gradient(135deg, rgba(255,180,230,0.28), rgba(255,140,215,0.28));border-color:var(--pink-soft);}
.nav-card.tint-pink:hover, .nav-card.tint-pink:focus-visible{border-color:var(--pink-vivid);}
.nav-card.tint-pink h3, .nav-card.tint-pink p{color:var(--pink-text);}
.nav-card.tint-mint{background:linear-gradient(135deg, rgba(140,230,190,0.28), rgba(90,215,160,0.28));border-color:#6FE0B0;}
.nav-card.tint-mint:hover, .nav-card.tint-mint:focus-visible{border-color:var(--mint-vivid);}
.nav-card.tint-mint h3, .nav-card.tint-mint p{color:var(--mint-text);}
.nav-card.tint-sky{background:linear-gradient(135deg, rgba(150,220,255,0.28), rgba(100,200,255,0.28));border-color:var(--sky-soft);}
.nav-card.tint-sky:hover, .nav-card.tint-sky:focus-visible{border-color:var(--sky-vivid);}
.nav-card.tint-sky h3, .nav-card.tint-sky p{color:var(--sky-text);}
.nav-card.tint-peach{background:linear-gradient(135deg, rgba(255,210,150,0.28), rgba(255,180,95,0.28));border-color:#FFC47A;}
.nav-card.tint-peach:hover, .nav-card.tint-peach:focus-visible{border-color:#FF9A1C;}
.nav-card.tint-peach h3, .nav-card.tint-peach p{color:#6D4717;}
.nav-card.tint-lemon{background:linear-gradient(135deg, rgba(255,235,150,0.28), rgba(255,220,80,0.28));border-color:#FFE066;}
.nav-card.tint-lemon:hover, .nav-card.tint-lemon:focus-visible{border-color:#F2C200;}
.nav-card.tint-lemon h3, .nav-card.tint-lemon p{color:#5C4A00;}
/* 6th tint, added for Adjust/Watermark. Deliberately reuses the site's own
   base purple tokens (--border-color/--accent/--text-color) instead of a
   one-off hex, the same way tint-pink reuses --pink-soft/--pink-vivid/
   --pink-text (the toolbar buttons' own pink) -- the purple card should
   read as "the same purple as the rest of the site," not its own distinct
   hue. #7D55E7 (accent) as plain rgb since there's no --accent-rgb token
   yet -- add one if a second use case needs it. */
.nav-card.tint-purple{background:linear-gradient(135deg, rgba(var(--border-color-rgb),0.28), rgba(125,85,231,0.28));border-color:var(--border-color);}
.nav-card.tint-purple:hover, .nav-card.tint-purple:focus-visible{border-color:var(--accent);}
.nav-card.tint-purple h3, .nav-card.tint-purple p{color:var(--text-color);}
.nav-card h3{
  font-family:var(--font-body);
  font-size:var(--font-size-base);
  font-weight:600;
  margin:0 0 4px;
  color:var(--text-color);
}
.nav-card p{
  font-family:var(--font-mono);
  font-size:var(--font-size-small);
  color:var(--text-color);
  margin:0;
}

/* Top spacing is margin, not padding. The old -2.5px value here only
   matched the header's wavy edge's own top-of-box position, not the
   visible stroke's actual bottom edge (measured by rendering the same SVG
   to canvas and reading pixel alpha) -- the true gap from the visible
   curve to the heading was only ~17px, not the site's standard 28px.
   8.45px is the corrected value: true-stroke-bottom-to-heading was
   measured at 17.05px with the old margin, so +10.95px (rounding to the
   canvas measurement's own precision) closes the gap to exactly 28px.
   Re-measure the same way (render the wavy SVG to an off-screen canvas,
   read alpha per row) if the header height, font-size, or line-height
   here ever changes. */
.hero{padding:0;margin-top:6.45px;text-align:center;}
.hero h1{
  font-family:var(--font-display);
  font-weight:600;
  font-size:1.4rem;
  margin:0 auto 16px;
  max-width:900px;
  line-height:var(--line-height);
  color:var(--accent);
  text-shadow:var(--text-shadow);
  text-wrap:balance;
}
.hero p{
  font-family:var(--font-body);
  font-weight:400;
  font-size:var(--font-size-base);
  color:var(--text-color);
  max-width:820px;
  margin:0 auto;
  line-height:var(--line-height);
}
.hero .updated{
  font-size:0.85rem;
  margin:10px auto 0;
}

.about-section{
  padding:22px 24px;
  background:var(--panel-bg);
  border:var(--border-default);
  border-radius:var(--radius);
  box-shadow:var(--shadow-panel);
}
.about-section .about-label{
  font-family:var(--font-body);
  font-weight:600;
  font-size:var(--font-size-base);
  line-height:var(--line-height);
  color:var(--text-color);
  margin:0 0 8px;
}
.about-section p{
  font-family:var(--font-mono);
  font-size:var(--font-size-small);
  color:var(--text-color);
  margin:0;
  line-height:var(--line-height);
}
