/* ==========================================================================
   WebTools design system: "Paper & Ink"
   Tokens, reset and the icon primitives. Load first.
   Split only to keep files small: the link order in every page is
   base, controls, home, shell, chrome, widgets, and the cascade depends on it.
   ========================================================================== */

@font-face {
  font-family: "Instrument Sans";
  src: url("fonts/instrument-sans-latin.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

/* --- 1. Tokens ----------------------------------------------------------- */

:root {
  /* colour: light. The head script always sets data-theme, so dark lives in
     one block below instead of being duplicated in a media query. */
  --bg: #fcfaf6;
  --bg-tint: #f5f1e9;
  --bg-deep: #efe9dd;
  --surface: #ffffff;
  --surface-2: #f5f1e9;
  --fg: #14161a;
  --fg-2: #3a3f47;
  --muted: #5c6068;
  --faint: #8b8f97;
  --border: #e4ded3;
  --accent: #ff5b22;
  --accent-on: #14161a;
  --accent-soft: #ffe9e0;
  --accent-glow: rgb(255 91 34 / 0.16);
  --safe: #0f8a6a;
  --safe-soft: #e4f4ef;
  --danger: #c0341b;
  --shadow-1: 0 1px 2px rgb(20 22 26 / 0.04);
  --shadow-2: 0 10px 30px -14px rgb(20 22 26 / 0.18);
  --shadow-pop: 0 24px 64px -16px rgb(20 22 26 / 0.3);
  --grid-line: rgb(20 22 26 / 0.045);

  /* type */
  --font-display: "Instrument Sans", ui-sans-serif, system-ui, sans-serif;
  --font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  --text-2xs: 0.6875rem;
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.375rem;
  --text-2xl: 1.75rem;
  --text-3xl: 2.25rem;
  --text-4xl: clamp(2rem, 3.4vw, 3rem);
  --text-hero: clamp(2.6rem, 6.2vw, 5.25rem);
  --leading-body: 1.62;
  --leading-tight: 1.04;
  --tracking-display: -0.026em;
  --tracking-hero: -0.038em;
  --tracking-label: 0.14em;

  /* space / shape */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;
  --s8: 64px;
  --s9: 96px;
  --s10: 128px;
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 18px;
  --r-full: 999px;

  /* layout: the gutter grows with the viewport, the content does not centre
     into a narrow column */
  --gutter: clamp(16px, 4.2vw, 76px);
  --max: 1840px;
  --measure: 66ch;
  --side-w: 268px;
  --ctl-h: 38px; /* every control in a toolbar row shares this height */
  --rail-w: 320px;
  --bar-h: 62px;

  /* motion. What each duration is for, so new tools stay consistent:
       dur-1  hover, focus and colour changes on controls
       dur-2  state changes: card lift, icon tile flip, palette fade
       dur-3  things that appear: palette box, drawer, stat chips
       dur-4  scroll reveals and the hero entrance
     Everything collapses to 0.01ms under prefers-reduced-motion. */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-back: cubic-bezier(0.34, 1.46, 0.64, 1);
  --dur-1: 130ms;
  --dur-2: 230ms;
  --dur-3: 420ms;
  --dur-4: 700ms;
}

html[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0d0f12;
  --bg-tint: #131619;
  --bg-deep: #090a0c;
  --surface: #161a1f;
  --surface-2: #1d2127;
  --fg: #eeebe5;
  --fg-2: #c4c8ce;
  --muted: #979ea7;
  --faint: #6d747d;
  --border: #262b32;
  --accent: #ff6e3c;
  --accent-on: #14161a;
  --accent-soft: #2c1a13;
  --accent-glow: rgb(255 110 60 / 0.2);
  --safe: #2ecb9e;
  --safe-soft: #0f2520;
  --danger: #ff7a68;
  --shadow-1: 0 1px 2px rgb(0 0 0 / 0.3);
  --shadow-2: 0 10px 30px -14px rgb(0 0 0 / 0.6);
  --shadow-pop: 0 24px 64px -16px rgb(0 0 0 / 0.75);
  --grid-line: rgb(255 255 255 / 0.04);
}

/* --- 2. Reset + base ----------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; }

* { margin: 0; }

/* Any author `display:` on a class outranks the user agent rule for [hidden],
   so every `display: flex` or `display: grid` utility silently defeats the
   attribute: `<p class="status" hidden>` stayed on screen. One override here
   beats remembering a per-class rule for each new widget. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--bar-h) + var(--s5));
  interpolate-size: allow-keywords; /* lets <details> animate open where supported */
  /* clip, not hidden: hidden would make this a scroll container and break
     every position:sticky descendant (the top bar and the sidebar) */
  overflow-x: clip;
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

img, svg, canvas { display: block; max-width: 100%; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
}
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-lg); letter-spacing: -0.014em; }
p { text-wrap: pretty; }

a { color: inherit; text-decoration-color: var(--border); text-underline-offset: 3px; }
a:hover { text-decoration-color: var(--accent); }

button, input, select, textarea { font: inherit; color: inherit; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

::selection { background: var(--accent); color: var(--accent-on); }

/* cross document page transitions where supported, ignored elsewhere */
@view-transition { navigation: auto; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* --- 3. Layout ----------------------------------------------------------- */

/* full bleed row: fills the viewport, gutters grow with it */
.bleed {
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* a full width tinted band, edge to edge */
.band {
  border-block: 1px solid var(--border);
  background: var(--bg-tint);
}

/* app shell: sidebar plus content, used on tool and category pages */
.shell {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
}
/* The sidebar is fixed, not a grid column: as a sticky element it was glued to
   the bottom of its column and rode up under the header at the end of the
   scroll. Fixed means it never moves, so the shell reserves its width instead. */
@media (min-width: 1060px) {
  .shell { padding-left: var(--side-w); }
}

.shell-main {
  min-width: 0;
  padding-bottom: var(--s8);
  /* layouts inside here measure this column, not the window, so the sidebar
     width is already accounted for */
  container: main / inline-size;
}
.tool-col { min-width: 0; container: col / inline-size; }

.skip {
  position: absolute;
  left: var(--s4);
  top: -100px;
  z-index: 100;
  background: var(--accent);
  color: var(--accent-on);
  padding: var(--s2) var(--s4);
  border-radius: var(--r-sm);
  font-weight: 600;
}
.skip:focus { top: var(--s2); }

.label {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--muted);
}

/* --- 4. Icons ------------------------------------------------------------ */

.ico {
  width: 20px;
  height: 20px;
  flex: none;
  stroke-width: 2;
  color: inherit;
}
.ico-sm { width: 16px; height: 16px; }
.ico-lg { width: 24px; height: 24px; }
.ico-xl { width: 30px; height: 30px; stroke-width: 1.7; }

/* icon in a tinted tile, the repeating visual unit of the whole site */
.ibox {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  flex: none;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--fg-2);
  transition: background var(--dur-2) var(--ease), color var(--dur-2) var(--ease),
    border-color var(--dur-2) var(--ease), transform var(--dur-2) var(--ease-back);
}
.ibox-sm { width: 32px; height: 32px; border-radius: var(--r-sm); }
.ibox-lg { width: 54px; height: 54px; border-radius: var(--r-lg); }

/* arrows that lean into the direction they point on hover */
.go {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.go .ico { transition: transform var(--dur-2) var(--ease); }
.go:hover .ico { transform: translateX(4px); }


/* ==========================================================================
   WebTools design system: "Paper & Ink"
   Top bar, buttons, inputs. Load second.
   Split only to keep files small: the link order in every page is
   base, controls, home, shell, chrome, widgets, and the cascade depends on it.
   ========================================================================== */

/* --- 5. Top bar ---------------------------------------------------------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  background: color-mix(in oklab, var(--bg) 84%, transparent);
  backdrop-filter: saturate(1.5) blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
  view-transition-name: topbar;
}
.topbar.is-stuck {
  border-bottom-color: var(--border);
  box-shadow: var(--shadow-1);
}

.topbar-row {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas:
    "brand actions"
    "cats cats";
  align-items: center;
  gap: var(--s2) var(--s6);
  min-height: var(--bar-h);
  /* Real padding, not only a minimum height. Below 1000px this row stacks
     into two and grows past the minimum, at which point the minimum provides
     no breathing room at all and the logo sits flush against the top edge of
     the window. At full width the padding and the minimum come to the same
     height, so nothing moves there. */
  padding-block: var(--s3);
}

.brand {
  grid-area: brand;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-lg);
  letter-spacing: -0.032em;
  text-decoration: none;
}
.brand-mark {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 9px;
  background: var(--accent);
  color: var(--accent-on);
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  font-weight: 700;
  line-height: 1;
  transition: transform var(--dur-2) var(--ease-back);
}
.brand:hover .brand-mark { transform: rotate(-8deg) scale(1.06); }

.topbar-actions {
  grid-area: actions;
  display: flex;
  align-items: center;
  gap: var(--s2);
  justify-self: end;
}

.cats {
  grid-area: cats;
  display: flex;
  align-items: center;
  gap: var(--s1);
  overflow-x: auto;
  scrollbar-width: none;
  margin-inline: calc(var(--gutter) * -1);
  padding: 0 var(--gutter) var(--s2);
}
.cats::-webkit-scrollbar { display: none; }
.cats a {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--fg-2);
  text-decoration: none;
  white-space: nowrap;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
    background var(--dur-1) var(--ease);
}
.cats a:hover { border-color: var(--accent); color: var(--fg); }
.cats a[aria-current="page"] { border-color: var(--accent); background: var(--accent-soft); color: var(--fg); }
.cats .ico { width: 15px; height: 15px; }

@media (min-width: 1000px) {
  .topbar-row {
    grid-template-columns: auto 1fr auto;
    grid-template-areas: "brand cats actions";
    gap: var(--s7);
  }
  .cats { overflow: visible; margin: 0; padding: 0; gap: var(--s5); }
  .cats a {
    padding: 4px 0;
    border: 0;
    border-radius: 0;
    background: none;
    position: relative;
  }
  .cats a::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -2px;
    height: 2px;
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--dur-2) var(--ease);
  }
  .cats a:hover::after,
  .cats a[aria-current="page"]::after { transform: scaleX(1); }
  .cats a:hover, .cats a[aria-current="page"] { background: none; }
  .cats .ico { display: none; }
}

/* --- 6. Buttons ---------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: var(--ctl-h);
  padding: 9px var(--s4);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--fg);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.15;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    transform var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.btn:hover { border-color: var(--fg-2); }
.btn:active { transform: translateY(1px) scale(0.99); }
.btn[disabled] { opacity: 0.45; cursor: not-allowed; }

.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-on); }
.btn-primary:hover {
  background: color-mix(in oklab, var(--accent), black 10%);
  border-color: color-mix(in oklab, var(--accent), black 10%);
}
.btn-ghost { background: transparent; border-color: transparent; color: var(--muted); }
.btn-ghost:hover { background: var(--surface-2); border-color: var(--border); color: var(--fg); }
.btn-lg { padding: 13px var(--s5); font-size: var(--text-base); }

.iconbtn {
  display: grid;
  place-items: center;
  width: var(--ctl-h);
  height: var(--ctl-h);
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--fg-2);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
    transform var(--dur-2) var(--ease-back);
}
.iconbtn:hover { border-color: var(--accent); color: var(--fg); transform: translateY(-1px); }

/* the theme toggle swaps icons with a small rotation */
.theme-ico { display: grid; place-items: center; width: 20px; height: 20px; }
.theme-ico .ico { grid-area: 1 / 1; transition: opacity var(--dur-2) var(--ease), transform var(--dur-3) var(--ease); }
html[data-theme="light"] .theme-ico .ico-moon,
html[data-theme="dark"] .theme-ico .ico-sun { opacity: 0; transform: rotate(-90deg) scale(0.6); }

.searchbtn {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  height: var(--ctl-h);
  min-width: var(--ctl-h);
  padding-inline: 10px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--text-sm);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
    box-shadow var(--dur-2) var(--ease);
}
.searchbtn:hover {
  border-color: var(--accent);
  color: var(--fg-2);
  box-shadow: 0 0 0 4px var(--accent-glow);
}
.searchbtn-text { display: none; }
@media (min-width: 560px) {
  .searchbtn { min-width: 230px; padding-inline: var(--s3); }
  .searchbtn-text { display: block; margin-right: auto; }
}

/* a static count chip, for things like "most repeated" word tallies */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  background: var(--bg-tint);
  font-size: var(--text-sm);
  color: var(--muted);
}
.pill b { color: var(--fg); font-weight: 600; font-family: var(--font-mono); font-size: var(--text-xs); }

kbd {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  line-height: 1;
  padding: 4px 6px;
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--muted);
}

.segment {
  display: inline-flex;
  align-items: stretch;
  min-height: var(--ctl-h);
  padding: 3px;
  gap: 2px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.segment button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 7px var(--s3);
  line-height: 1.15;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--muted);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.segment button[aria-pressed="true"] {
  background: var(--surface);
  color: var(--fg);
  box-shadow: inset 0 0 0 1px var(--border), var(--shadow-1);
}

/* --- 7. Inputs ----------------------------------------------------------- */

.field { display: grid; gap: 7px; align-content: start; }
.field > label,
.field-label { font-size: var(--text-sm); font-weight: 600; color: var(--fg-2); font-family: var(--font-display); }

.input, .textarea, .select {
  width: 100%;
  padding: 10px var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--fg);
  font-size: var(--text-base);
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.input::placeholder, .textarea::placeholder { color: var(--faint); }
.input:focus-visible, .textarea:focus-visible, .select:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-glow);
}
.input, .select { min-height: var(--ctl-h); line-height: 1.15; }

/* The native select arrow sits jammed inside the padding and cannot follow the
   theme. Hide it and place our own chevron, which inherits currentColor. */
.selectwrap { position: relative; display: inline-flex; align-items: center; }
.selectwrap .select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 34px;
  font-size: var(--text-sm);
  cursor: pointer;
}
.selectwrap > .ico {
  position: absolute;
  right: 11px;
  color: var(--muted);
  pointer-events: none;
  transition: color var(--dur-1) var(--ease);
}
.selectwrap:hover > .ico { color: var(--fg-2); }
.selectwrap .select:disabled { opacity: 0.45; cursor: not-allowed; }
.selectwrap:has(.select:disabled) > .ico { opacity: 0.45; }

.textarea {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.6;
  resize: vertical;
  min-height: 240px;
  tab-size: 2;
}

.dropzone {
  display: grid;
  place-items: center;
  gap: var(--s2);
  padding: var(--s7) var(--s5);
  border: 1.5px dashed var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  text-align: center;
  cursor: pointer;
  transition: border-color var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}
.dropzone:hover, .dropzone.is-over { border-color: var(--accent); background: var(--accent-soft); }
.is-over { border-color: var(--accent) !important; }


/* ==========================================================================
   WebTools design system: "Paper & Ink"
   Hero, drifting wall, bento tiles, tool cards. Load third.
   Split only to keep files small: the link order in every page is
   base, controls, home, shell, chrome, widgets, and the cascade depends on it.
   ========================================================================== */

/* --- 8. Hero ------------------------------------------------------------- */

.hero {
  position: relative;
  padding: clamp(var(--s7), 7vw, var(--s10)) 0 var(--s6);
  overflow: hidden;
}
/* faint engineering grid, sits behind everything and fades out downward */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 46px 46px;
  mask-image: radial-gradient(120% 90% at 20% 0%, #000 20%, transparent 75%);
  pointer-events: none;
}
.hero-in {
  position: relative;
  display: grid;
  gap: var(--s7);
  align-items: center;
}
@media (min-width: 1000px) {
  .hero-in { grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); gap: var(--s8); }
}

.hero h1 {
  font-size: var(--text-hero);
  font-weight: 700;
  letter-spacing: var(--tracking-hero);
  margin: var(--s4) 0 var(--s5);
  max-width: 16ch;
}
.hero h1 .w {
  display: inline-block;
  animation: rise var(--dur-4) var(--ease) backwards;
  animation-delay: calc(var(--i) * 70ms);
}
@keyframes rise {
  from { opacity: 0; transform: translateY(0.4em) rotate(1.5deg); }
}
.hero h1 em { font-style: normal; color: var(--accent); }

.hero-lede {
  font-size: clamp(1.0625rem, 1.3vw, 1.25rem);
  color: var(--muted);
  max-width: 46ch;
  margin-bottom: var(--s6);
}

/* the search field: a button that looks like the most important input here */
.hero-search {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  max-width: 560px;
  padding: var(--s4) var(--s5);
  border: 1.5px solid var(--fg);
  border-radius: var(--r-lg);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--text-lg);
  text-align: left;
  cursor: text;
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease),
    transform var(--dur-2) var(--ease);
}
.hero-search:hover {
  border-color: var(--accent);
  box-shadow: 0 0 0 5px var(--accent-glow), var(--shadow-2);
  transform: translateY(-2px);
}
.hero-search span { margin-right: auto; }
.hero-search .ico { color: var(--fg); }

.hero-jump {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s3);
  margin-top: var(--s5);
  font-size: var(--text-sm);
}
/* the render target must not become a flex item of its own */
.hero-jump .jumps { display: contents; }
.hero-jump a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--fg-2);
  text-decoration: none;
  padding: 4px 0;
  border-bottom: 1px solid var(--border);
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.hero-jump a:hover { color: var(--accent); border-color: var(--accent); }

/* --- 9. Drifting wall of tools (hero right) ------------------------------ */

.wall {
  display: grid;
  gap: var(--s3);
  overflow: hidden;
  mask-image: linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent),
    linear-gradient(#000, #000);
  mask-composite: intersect;
}
@media (min-width: 1000px) {
  .wall {
    mask-image: linear-gradient(90deg, transparent, #000 26%, #000 100%),
      linear-gradient(transparent, #000 18%, #000 82%, transparent);
    mask-composite: intersect;
  }
}
.wall-row {
  display: flex;
  gap: var(--s3);
  width: max-content;
  animation: drift var(--speed, 70s) linear infinite;
  will-change: transform;
}
.wall-row.rev { animation-direction: reverse; }
/* five rows fill the hero beside the headline, but that is a lot of decoration
   to scroll past on a phone */
@media (max-width: 999px) {
  .wall-row:nth-child(n + 4) { display: none; }
}
@keyframes drift { to { transform: translateX(-50%); } }
.wall:hover .wall-row,
.wall:focus-within .wall-row { animation-play-state: paused; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: 10px var(--s4) 10px var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--fg-2);
  font-size: var(--text-sm);
  font-weight: 500;
  white-space: nowrap;
  text-decoration: none;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
    background var(--dur-1) var(--ease), transform var(--dur-2) var(--ease-back);
}
.chip:hover {
  border-color: var(--accent);
  color: var(--fg);
  background: var(--surface);
  transform: translateY(-2px);
}
.chip .ico { width: 16px; height: 16px; color: var(--faint); }
.chip:hover .ico { color: var(--accent); }

/* --- 10. Section heads --------------------------------------------------- */

.section { padding-block: var(--s8); }
.section-tight { padding-block: var(--s7); }
.section-head {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: var(--s5);
  padding-bottom: var(--s4);
  margin-bottom: var(--s5);
  border-bottom: 1px solid var(--border);
}
.section-head h2 { font-size: var(--text-3xl); }
.section-head .label { margin-bottom: 6px; }

/* --- 11. Bento category grid --------------------------------------------- */

.bento {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 640px) { .bento { grid-template-columns: minmax(0, 1fr); } }
@media (min-width: 641px) and (max-width: 1099px) {
  .tile-full { grid-column: span 2; }
  /* two per row: cap the tool list so neighbouring tiles end up close in
     height instead of one stretching to match the other */
  .tile-tools a:nth-child(n + 5) { display: none; }
}
/* widths vary, heights do not: a row-spanning tile leaves a hole whose size
   depends on how many tools that category happens to have */
@media (min-width: 1100px) {
  .bento { grid-template-columns: repeat(12, minmax(0, 1fr)); gap: var(--s5); }
  .tile-full { grid-column: span 12; }
  .tile-wide { grid-column: span 6; }
  .tile-sm { grid-column: span 3; }
}

.tile {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  padding: var(--s5);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  overflow: hidden;
  transition: border-color var(--dur-2) var(--ease), transform var(--dur-3) var(--ease),
    box-shadow var(--dur-3) var(--ease);
}
/* pointer spotlight, --mx and --my come from app.js */
.tile::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(420px circle at var(--mx, 50%) var(--my, 0%), var(--accent-glow), transparent 62%);
  opacity: 0;
  transition: opacity var(--dur-3) var(--ease);
  pointer-events: none;
}
.tile:hover { border-color: color-mix(in oklab, var(--accent) 55%, var(--border)); box-shadow: var(--shadow-2); }
.tile:hover::after { opacity: 1; }
.tile:focus-within { border-color: var(--accent); }

.tile-head { display: flex; align-items: center; gap: var(--s3); }
.tile-head h3 { font-size: var(--text-xl); }
.tile:hover .ibox { background: var(--accent); border-color: var(--accent); color: var(--accent-on); transform: rotate(-6deg); }
.tile-count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted);
}
.tile-desc { color: var(--muted); font-size: var(--text-sm); }

.tile-tools { display: grid; gap: 2px; }
.tile-tools a {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 9px var(--s3);
  margin-inline: calc(var(--s3) * -1);
  border-radius: var(--r-sm);
  font-size: var(--text-sm);
  color: var(--fg-2);
  text-decoration: none;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.tile-tools a .ico { width: 16px; height: 16px; color: var(--faint); transition: color var(--dur-1) var(--ease); }
.tile-tools a .ico-arrow {
  margin-left: auto;
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.tile-tools a:hover { background: var(--surface-2); color: var(--fg); }
.tile-tools a:hover .ico { color: var(--accent); }
.tile-tools a:hover .ico-arrow { opacity: 1; transform: none; }
/* an empty category still says something true, rather than leaving a hole
   next to a row-mate that has tools in it */
.tile-soon {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 9px 0;
  font-size: var(--text-sm);
  color: var(--muted);
}
.tile-soon .ico { color: var(--accent); }

.tile-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); margin-top: auto; padding-top: var(--s2); }
@media (min-width: 1100px) {
  .tile-full { padding: var(--s6); }
  .tile-wide .tile-tools { grid-template-columns: 1fr 1fr; gap: 2px var(--s5); }
  .tile-full .tile-tools { grid-template-columns: repeat(3, 1fr); gap: 2px var(--s6); }
}

/* --- 12. Tool cards ------------------------------------------------------ */

.grid {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
}
.grid-tight { grid-template-columns: repeat(auto-fill, minmax(252px, 1fr)); gap: var(--s3); }

.card {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s3) var(--s4);
  align-items: start;
  padding: var(--s4) var(--s5) var(--s5);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  text-decoration: none;
  overflow: hidden;
  transition: border-color var(--dur-2) var(--ease), transform var(--dur-2) var(--ease),
    box-shadow var(--dur-2) var(--ease);
}
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(300px circle at var(--mx, 50%) var(--my, 0%), var(--accent-glow), transparent 60%);
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease);
}
.card:hover {
  border-color: color-mix(in oklab, var(--accent) 55%, var(--border));
  transform: translateY(-3px);
  box-shadow: var(--shadow-2);
}
.card:hover::after { opacity: 1; }
.card:hover .ibox { background: var(--accent); border-color: var(--accent); color: var(--accent-on); transform: rotate(-6deg); }

.card-body { min-width: 0; grid-column: 1 / -1; }
.card-top { display: flex; align-items: center; gap: var(--s3); width: 100%; min-width: 0; grid-column: 1 / -1; }
.card-top .card-name { flex: 1; min-width: 0; }
.card-soon, .card .ico-arrow { flex: none; }
.card-name {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 600;
  letter-spacing: -0.014em;
  color: var(--fg);
}
.card-desc { display: block; font-size: var(--text-sm); line-height: 1.5; color: var(--muted); }
.card .ico-arrow {
  margin-left: auto;
  color: var(--faint);
  opacity: 0;
  transform: translate(-6px, 6px);
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease), color var(--dur-2) var(--ease);
}
.card:hover .ico-arrow { opacity: 1; transform: none; color: var(--accent); }
.card-soon {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: 0.08em;
  color: var(--faint);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  padding: 2px 8px;
  margin-left: auto;
}
.card:has(.card-soon) .ico-arrow { display: none; }

/* --- 13. Horizontal snap rail (popular tools) ---------------------------- */

.snap {
  display: flex;
  gap: var(--s4);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  padding-bottom: var(--s2);
  /* runs past the right gutter so it is obvious there is more */
  margin-right: calc(var(--gutter) * -1);
  padding-right: var(--gutter);
}
.snap::-webkit-scrollbar { display: none; }
.snap > * { scroll-snap-align: start; flex: 0 0 clamp(240px, 26vw, 320px); }


/* ==========================================================================
   WebTools design system: "Paper & Ink"
   Sidebar, palette, tool page, prose. Load fourth. Widgets belonging to a
   single tool live in widgets.css, not here.
   Split only to keep files small: the link order in every page is
   base, controls, home, shell, chrome, widgets, and the cascade depends on it.
   ========================================================================== */

/* --- 14. Sidebar (app shell) --------------------------------------------- */

.side {
  border-right: 1px solid var(--border);
  /* the same background as the page and the top bar, so the brand and the menu
     under it read as one column instead of two stacked panels. The hairline on
     the right is what separates the column, not a fill. This also matches the
     mobile drawer, which was already on --bg. */
  background: var(--bg);
}
.side-in {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s5) var(--s4) var(--s7);
  display: grid;
  gap: var(--s1);
  align-content: start;
}

@media (min-width: 1060px) {
  .side {
    position: fixed;
    left: 0;
    top: var(--bar-h);
    bottom: 0;
    width: var(--side-w);
    z-index: 30;
  }
  /* its own scrollbar, so a long tool list never pushes the page around */
  .side-in { height: 100%; }
  /* keep the footer clear of the fixed column. Padding, not margin, so the
     background still spans edge to edge while the content lines up with the
     main column above it. */
  .shell ~ .footer { padding-left: var(--side-w); }
}
.side-title { padding: 0 var(--s3) var(--s3); }

.side details { border-radius: var(--r-sm); }
.side summary {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 9px var(--s3);
  border-radius: var(--r-sm);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--fg-2);
  cursor: pointer;
  list-style: none;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.side summary::-webkit-details-marker { display: none; }
.side summary:hover { background: var(--surface); color: var(--fg); }
.side summary .ico-chev { margin-left: auto; color: var(--faint); transition: transform var(--dur-2) var(--ease); }
.side details[open] > summary { color: var(--fg); }
.side details[open] > summary .ico-chev { transform: rotate(90deg); }
.side details[open] > summary .ico:first-child { color: var(--accent); }

.side-list { list-style: none; margin: 0; padding: var(--s1) 0 var(--s3) var(--s5); display: grid; gap: 1px; }
.side-list a {
  display: block;
  padding: 7px var(--s3);
  border-left: 1px solid var(--border);
  font-size: var(--text-sm);
  color: var(--muted);
  text-decoration: none;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
    background var(--dur-1) var(--ease), padding-left var(--dur-1) var(--ease);
}
.side-list a:hover { color: var(--fg); border-color: var(--accent); padding-left: var(--s4); background: var(--surface); }
.side-list a[aria-current="page"] {
  color: var(--fg);
  font-weight: 600;
  border-left: 2px solid var(--accent);
  background: var(--surface);
}
.side-list .soon { color: var(--faint); }

/* animate the accordion where interpolate-size is supported */
.side details::details-content {
  block-size: 0;
  overflow: hidden;
  transition: block-size var(--dur-2) var(--ease), content-visibility var(--dur-2) allow-discrete;
}
.side details[open]::details-content { block-size: auto; }

/* mobile: the sidebar becomes a drawer */
.side-toggle { display: inline-flex; }
@media (max-width: 1059px) {
  .side {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 50;
    width: min(320px, 86vw);
    border-right: 1px solid var(--border);
    transform: translateX(-102%);
    transition: transform var(--dur-3) var(--ease);
    box-shadow: var(--shadow-pop);
  }
  .side.is-open { transform: none; }
  .side-in { top: 0; max-height: 100vh; padding-top: var(--s4); }
  .side-scrim {
    position: fixed;
    inset: 0;
    z-index: 49;
    background: color-mix(in oklab, var(--fg) 40%, transparent);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-2) var(--ease);
  }
  .side-scrim.is-open { opacity: 1; pointer-events: auto; }
}
@media (min-width: 1060px) {
  .side-toggle, .side-scrim, .side-close { display: none; }
}
.side-close { margin-left: auto; }
.side-head { display: flex; align-items: center; gap: var(--s3); padding: 0 var(--s3) var(--s3); }

/* --- 15. Command palette ------------------------------------------------- */

.palette {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  align-items: start;
  justify-items: center;
  padding: var(--s4);
  background: color-mix(in oklab, var(--bg-deep) 62%, transparent);
  backdrop-filter: blur(6px);
  animation: fade var(--dur-2) var(--ease);
}
.palette[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } }
@media (min-width: 640px) { .palette { padding-top: 11vh; } }

.palette-box {
  width: 100%;
  max-width: 660px;
  max-height: min(72vh, 620px);
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--shadow-pop);
  overflow: hidden;
  animation: pop var(--dur-3) var(--ease);
}
@keyframes pop { from { opacity: 0; transform: translateY(-14px) scale(0.97); } }

.palette-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--border);
  color: var(--muted);
}
.palette-input { flex: 1; border: 0; background: none; font-size: var(--text-lg); outline: none; min-width: 0; }
.palette-list { list-style: none; margin: 0; padding: var(--s2); overflow-y: auto; overscroll-behavior: contain; }
.palette-group {
  padding: var(--s3) var(--s3) var(--s1);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--faint);
}
.palette-item > a {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 10px var(--s3);
  border-radius: var(--r-sm);
  text-decoration: none;
}
.palette-item .ibox { width: 32px; height: 32px; border-radius: var(--r-sm); }
.palette-item .ibox .ico { width: 17px; height: 17px; }
.palette-item-name { font-weight: 500; }
.palette-item-cat { margin-left: auto; font-size: var(--text-xs); color: var(--faint); }
.palette-item[aria-selected="true"] > a { background: var(--accent-soft); }
.palette-item[aria-selected="true"] .ibox { border-color: var(--accent); color: var(--accent); }
.palette-item[aria-selected="true"] .palette-item-cat { color: var(--accent); }
.palette-empty { padding: var(--s7) var(--s4); text-align: center; color: var(--muted); }
.palette-foot {
  display: flex;
  gap: var(--s4);
  padding: 11px var(--s5);
  border-top: 1px solid var(--border);
  background: var(--bg-tint);
  font-size: var(--text-xs);
  color: var(--muted);
}
.palette-foot span { display: inline-flex; align-items: center; gap: 6px; }
@media (max-width: 560px) { .palette-foot { display: none; } }

/* --- 16. Tool page ------------------------------------------------------- */

.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  /* Tight on purpose. Everything above the tool is space the visitor did not
     come for, and on a laptop this used to push the working area below the
     fold on every single tool page. */
  padding: var(--s4) 0 var(--s3);
  font-size: var(--text-sm);
  color: var(--muted);
}
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--accent); }
.crumbs [aria-current] { color: var(--fg-2); }
.crumbs .ico { width: 14px; height: 14px; color: var(--faint); }

.tool-wrap { display: grid; gap: var(--s7); align-items: start; grid-template-columns: minmax(0, 1fr); }
/* the rail only appears once the tool still gets a comfortable width beside it */
@container main (min-width: 1400px) {
  .tool-wrap { grid-template-columns: minmax(0, 1fr) var(--rail-w); gap: var(--s8); }
}

/* The title and the privacy badge share one row, and nothing else lives up
   here. People arriving from a search have already read the title in the
   search result: what they want is the box they can paste into, on screen,
   without scrolling. The explanatory paragraph that used to sit here now sits
   under the tool, where it still counts for search engines and no longer
   costs a screenful before anything useful appears. */
.tool-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3) var(--s4);
}
.tool-head h1 {
  margin: 0;
  /* smaller than the page scale's 4xl: a tool page is not an article */
  font-size: clamp(1.75rem, 2.6vw, 2.25rem);
}
.tool-head .badge { margin: 0; }

/* the relocated lede, now a summary under the tool rather than a wall above */
.tool-intro {
  max-width: var(--measure);
  margin-top: var(--s6);
  color: var(--fg-2);
  font-size: var(--text-lg);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 13px 5px 10px;
  border: 1px solid color-mix(in oklab, var(--safe) 34%, var(--border));
  border-radius: var(--r-full);
  background: var(--safe-soft);
  color: color-mix(in oklab, var(--safe), var(--fg) 22%);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}
.badge .ico { width: 14px; height: 14px; color: var(--safe); }

.tool {
  margin-top: var(--s4);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--shadow-1);
  overflow: hidden;
}
.tool-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--border);
  background: var(--bg-tint);
}
.tool-bar-spacer { margin-left: auto; }
.tool-bar-actions { display: flex; gap: var(--s2); }
/* a phone cannot fit the mode switch, the indent picker and both actions on
   one line, and a grid here overflowed instead of wrapping */
@media (max-width: 620px) {
  .tool-bar { gap: 10px; }
  .tool-bar .segment { width: 100%; }
  .tool-bar .segment button { flex: 1; justify-content: center; }
  .tool-bar label.row { flex-wrap: nowrap; }
  .tool-bar-actions { flex: 1; justify-content: flex-end; }
  .tool-bar-spacer { display: none; }
}
.tool-body { padding: var(--s5); display: grid; gap: var(--s4); }
.tool-body .textarea { background: var(--bg-tint); }

.tool-io { display: grid; gap: var(--s4); }
@container col (min-width: 820px) {
  .tool-io { grid-template-columns: 1fr 1fr; }
  .tool-io > .field { height: 100%; grid-template-rows: auto 1fr; }
  .tool-io .textarea { height: 100%; }
}
/* pages without the app shell still need the side by side layout */
@media (min-width: 900px) {
  .tool-wrap:not(.tool-col *) .tool-io { grid-template-columns: 1fr 1fr; }
}

.result {
  display: flex;
  flex-direction: column;
  height: 100%;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg-tint);
  overflow: hidden;
}
.result-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--border);
}
.result-head .label { margin-right: auto; }
.result pre {
  flex: 1;
  margin: 0;
  padding: var(--s4);
  min-height: 180px;
  max-height: 420px;
  overflow: auto;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.6;
  tab-size: 2;
  white-space: pre;
}
.result pre:empty::before { content: attr(data-empty); color: var(--faint); }

.status {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted);
}
.status .ico { width: 14px; height: 14px; }
.status.is-ok { color: var(--safe); }
.status.is-bad { color: var(--danger); }

.stats { display: flex; flex-wrap: wrap; gap: var(--s2); list-style: none; padding: 0; }
.stats li {
  padding: 6px 11px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg-tint);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted);
  animation: rise var(--dur-3) var(--ease) backwards;
  animation-delay: calc(var(--i, 0) * 45ms);
}
.stats b { color: var(--fg); font-weight: 600; }

/* copy button swaps to a tick when it fires */
.copybtn .ico-check { display: none; }
.copybtn.is-done { color: var(--safe); border-color: color-mix(in oklab, var(--safe) 40%, var(--border)); }
.copybtn.is-done .ico-check { display: block; }
.copybtn.is-done .ico-copy { display: none; }

/* --- 17. Prose ----------------------------------------------------------- */

.prose { max-width: var(--measure); }
.prose h2 { font-size: var(--text-xl); margin-bottom: var(--s3); }
.prose > * + h2 { margin-top: var(--s6); }
.prose p { margin-bottom: var(--s4); color: var(--fg-2); }
.prose ol { padding-left: 0; list-style: none; display: grid; gap: var(--s3); counter-reset: step; }
.prose ol li {
  position: relative;
  padding-left: 38px;
  color: var(--fg-2);
  counter-increment: step;
}
.prose ol li::before {
  content: counter(step);
  position: absolute;
  left: 0;
  top: 2px;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: var(--r-full);
  background: var(--accent-soft);
  color: color-mix(in oklab, var(--accent), var(--fg) 35%);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 700;
}
.prose code { font-family: var(--font-mono); font-size: 0.9em; background: var(--surface-2); padding: 1px 5px; border-radius: 4px; }

/* supporting content sits side by side so the page does not just scroll */
.dual, .trio { display: grid; gap: var(--s7); }
@media (min-width: 1000px) { .dual { grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr); gap: var(--s8); } }
@container col (min-width: 660px) { .trio { grid-template-columns: 1fr 1fr; gap: var(--s7); } }
@container col (min-width: 980px) { .trio { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.trio > * { min-width: 0; }

.faq { display: grid; gap: var(--s2); }
.faq details {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  transition: border-color var(--dur-2) var(--ease);
}
.faq details:hover { border-color: color-mix(in oklab, var(--accent) 40%, var(--border)); }
.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s4) var(--s5);
  font-family: var(--font-display);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
/* the question is an h3, so each answer is its own passage; inside the flex
   summary it has to look like the summary text it replaced */
.faq summary h3 { margin: 0; font: inherit; }
.faq summary .ico { color: var(--accent); transition: transform var(--dur-2) var(--ease); }
.faq details[open] summary .ico { transform: rotate(135deg); }
.faq details > div { padding: 0 var(--s5) var(--s5); color: var(--fg-2); }


/* ==========================================================================
   WebTools design system: "Paper & Ink"
   Ads, trust band, toast, footer, motion helpers. Load last.
   Split only to keep files small: the link order in every page is
   base, controls, home, shell, chrome, widgets, and the cascade depends on it.
   ========================================================================== */

/* --- 18. Ads + rail ------------------------------------------------------ */

.ad {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 104px;
  padding: var(--s5);
  border: 1px dashed color-mix(in oklab, var(--border) 60%, var(--muted));
  border-radius: var(--r-md);
  background: var(--bg-tint);
  color: var(--faint);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.ad::before { content: "ad"; position: absolute; top: 7px; left: 11px; opacity: 0.7; }
.ad-below { margin-top: var(--s6); min-height: 120px; }
.ad-rail { min-height: 520px; }

.rail { display: none; }
@container main (min-width: 1400px) {
  /* stretch so the sticky panel below can travel the whole content column;
     align-content keeps the boxes themselves at the top */
  .rail { display: grid; gap: var(--s5); align-self: stretch; align-content: start; }
}
/* The rail as a whole is taller than a laptop viewport, and sticky cannot pin
   something taller than the screen. So the ad scrolls by normally and only the
   short "more tools" panel follows you down the page. */
.rail-box { position: sticky; top: calc(var(--bar-h) + var(--s5)); }
.rail-box { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); overflow: hidden; }
.rail-box > .label { display: block; padding: var(--s3) var(--s4); border-bottom: 1px solid var(--border); }
.rail-list { list-style: none; margin: 0; padding: var(--s2); display: grid; gap: 1px; }
.rail-list a {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 8px var(--s2);
  border-radius: var(--r-sm);
  font-size: var(--text-sm);
  text-decoration: none;
  color: var(--fg-2);
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.rail-list a:hover { background: var(--accent-soft); color: var(--fg); }
.rail-list .ico { width: 16px; height: 16px; color: var(--faint); }
.rail-list a:hover .ico { color: var(--accent); }

/* --- 19. Trust band ------------------------------------------------------ */

.trust {
  display: grid;
  gap: var(--s6);
  padding-block: var(--s7);
}
@media (min-width: 860px) { .trust { grid-template-columns: repeat(3, 1fr); gap: var(--s8); } }
.trust > div { display: grid; gap: var(--s3); align-content: start; }
.trust h3 { font-size: var(--text-lg); }
.trust p { color: var(--muted); font-size: var(--text-sm); }
.trust .ibox { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 30%, var(--border)); background: var(--accent-soft); }

/* --- 20. Toast ----------------------------------------------------------- */

.toast {
  position: fixed;
  left: 50%;
  bottom: var(--s6);
  z-index: 70;
  transform: translate(-50%, 14px);
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 11px var(--s5);
  border-radius: var(--r-full);
  background: var(--fg);
  color: var(--bg);
  font-size: var(--text-sm);
  font-weight: 500;
  box-shadow: var(--shadow-pop);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease-back);
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
.toast .ico { color: var(--accent); }

/* --- 21. Footer ---------------------------------------------------------- */

.footer {
  margin-top: var(--s9);
  border-top: 1px solid var(--border);
  background: var(--bg-tint);
  padding: var(--s8) 0 var(--s6);
}
.footer-top { display: grid; gap: var(--s6); padding-bottom: var(--s6); border-bottom: 1px solid var(--border); }
@media (min-width: 640px) { .footer-top { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1100px) { .footer-top { grid-template-columns: repeat(6, 1fr); } }
.footer-col { display: grid; gap: var(--s2); align-content: start; }
.footer-col a { font-size: var(--text-sm); color: var(--muted); text-decoration: none; line-height: 1.5; }
.footer-col a:hover { color: var(--accent); }
.footer-col a.is-head {
  display: flex;
  align-items: center;
  gap: var(--s2);
  color: var(--fg);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-sm);
  margin-bottom: var(--s1);
}
.footer-col a.is-head .ico { width: 16px; height: 16px; color: var(--accent); }
.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3) var(--s5);
  padding-top: var(--s5);
  font-size: var(--text-sm);
  color: var(--muted);
}
.footer-bottom nav { display: flex; flex-wrap: wrap; gap: var(--s4); margin-left: auto; }
.footer-bottom a { color: var(--muted); text-decoration: none; }
.footer-bottom a:hover { color: var(--accent); }

/* --- 22. Motion utilities ------------------------------------------------ */

/* scroll reveal. Only armed when JS is running, so no-JS never hides content. */
@media (prefers-reduced-motion: no-preference) {
  .js .reveal {
    opacity: 0;
    transform: translateY(18px);
    transition: opacity var(--dur-4) var(--ease), transform var(--dur-4) var(--ease);
    /* capped so a long grid does not cascade for seconds */
    transition-delay: calc(min(var(--i, 0), 6) * 45ms);
  }
  .js .reveal.in { opacity: 1; transform: none; }
}

/* --- 23. Utilities ------------------------------------------------------- */

.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.mono { font-family: var(--font-mono); }
.muted { color: var(--muted); }
.row { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.stack { display: grid; gap: var(--s4); }
.hr { height: 1px; background: var(--border); border: 0; }

/* --- 20. Full size view -------------------------------------------------- */

/* A native dialog, so Escape, focus trapping and the backdrop are the
   platform's job rather than ours. */
.bigview {
  /* A modal dialog is centred by the user agent with `inset: 0` and
     `margin: auto`. The reset's `* { margin: 0 }` wins over that, so the
     dialog collapsed into the top left corner of the window on every screen
     wide enough for it to be narrower than the viewport. Put the auto back. */
  margin: auto;
  width: min(1100px, 94vw);
  max-width: none;
  max-height: 88vh;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  color: var(--fg);
  box-shadow: var(--shadow-pop);
  overflow: hidden;
}
.bigview::backdrop {
  background: color-mix(in oklab, var(--bg-deep) 62%, transparent);
  backdrop-filter: blur(6px);
}
.bigview-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--border);
}
.bigview-head .label { margin-right: auto; }
.bigview-actions { display: flex; align-items: center; gap: var(--s2); }
.bigview-body {
  max-height: calc(88vh - 60px);
  overflow: auto;
  padding: var(--s4);
  background: var(--bg-tint);
}
/* inside the dialog the pane has room, so let it grow */
.bigview-body pre {
  max-height: none;
  min-height: 0;
  padding: 0;
  white-space: pre-wrap;
  word-break: break-word;
}
.bigview-body .tbl-wrap { max-height: none; border: 0; }
.bigview-body .cases { gap: var(--s2); }

/* the button that opens it: in a result header, or floating over a pane that
   has no header of its own */
.bigview-float {
  position: absolute;
  top: var(--s2);
  right: var(--s2);
  z-index: 2;
  background: var(--surface);
  box-shadow: var(--shadow-1);
}

/* ==========================================================================
   WebTools design system: "Paper & Ink"
   Widgets that belong to one tool each: the case list, the password display.
   Split only to keep files small: the concatenation order is
   base, controls, home, shell, chrome, widgets, and the cascade depends on it.
   ========================================================================== */

/* --- 21. Case converter -------------------------------------------------- */

/* one row per conversion: label, single line preview, copy. The preview is
   nowrap so a pasted list stays one row tall; copy still yields the breaks. */
.cases { list-style: none; padding: 0; display: grid; gap: var(--s2); }
.cases li {
  display: grid;
  grid-template-columns: 124px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s3);
  padding: 5px 5px 5px var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg-tint);
  animation: rise var(--dur-3) var(--ease) backwards;
  animation-delay: calc(var(--i, 0) * 35ms);
}
/* the label is itself an example, so it must not be uppercased by .label */
.cases .label { text-transform: none; }
.case-out {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.case-out:empty::before { content: attr(data-empty); color: var(--faint); }
@container col (max-width: 560px) {
  .cases li { grid-template-columns: minmax(0, 1fr) auto; }
  .cases .label { grid-column: 1 / -1; }
}

/* --- 22. Password generator ----------------------------------------------- */

/* the generated password: big, monospaced, with digits and symbols tinted so
   they can be read back off the screen without squinting */
.pw {
  display: block;
  min-height: 64px;
  padding: var(--s4) var(--s5);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg-tint);
  font-family: var(--font-mono);
  font-size: var(--text-xl);
  line-height: 1.5;
  word-break: break-all;
}
.pw .is-num { color: var(--accent); }
.pw .is-sym { color: var(--safe); }

.meter { display: flex; align-items: center; gap: 6px; }
.meter i {
  width: 40px;
  height: 5px;
  border-radius: 3px;
  background: var(--border);
  transition: background var(--dur-2) var(--ease);
}
.meter span {
  margin-left: var(--s2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted);
}
.meter[data-level="1"] i:nth-child(-n + 1),
.meter[data-level="2"] i:nth-child(-n + 2) { background: var(--danger); }
.meter[data-level="3"] i:nth-child(-n + 3) { background: var(--accent); }
.meter[data-level="4"] i { background: var(--safe); }

/* native range and checkbox, tinted. No custom control to maintain. */
.range { width: 100%; max-width: 420px; accent-color: var(--accent); }
.checks { display: flex; flex-wrap: wrap; gap: var(--s3) var(--s5); }
.check {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: var(--text-sm);
  color: var(--fg-2);
  cursor: pointer;
}
.check input { width: 16px; height: 16px; accent-color: var(--accent); }
.check code { font-size: var(--text-xs); color: var(--faint); }


/* --- 23. Regex tester ---------------------------------------------------- */

/* the slashes belong to the pattern, so show them rather than making people
   type them and then stripping them back off */
.slashed { display: flex; align-items: center; gap: var(--s2); }
.slashed > span { font-family: var(--font-mono); font-size: var(--text-lg); color: var(--faint); }
.slashed .input { flex: 1; font-family: var(--font-mono); }
.input[aria-invalid="true"] { border-color: var(--danger); }

/* JSON wants its lines left alone, but a URL or a Base64 blob is one very long
   line and reads better wrapped than clipped */
.result pre.wrap { white-space: pre-wrap; word-break: break-all; }

.hl { white-space: pre-wrap; word-break: break-word; }
.hl mark {
  background: var(--accent-soft);
  color: var(--fg);
  border-radius: 3px;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 45%, transparent);
}

/* capture groups get their own row of the match, so a long list of them does
   not squeeze the matched text */
.groups { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: var(--s2); }
.groups .pill { font-size: var(--text-2xs); }

/* --- 24. Calculators ------------------------------------------------------ */

/* input rows: as many columns as fit, one per field on a phone */
.calc-grid {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
/* the derived numbers. One box per figure, the headline one tinted. */
.figures {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(176px, 1fr));
  list-style: none;
  padding: 0;
  margin: 0;
}
/* cases that hold their own inputs need room to breathe */
.figures-wide { grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); }
.figures li {
  padding: var(--s3) var(--s4);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg-tint);
  animation: rise var(--dur-3) var(--ease) backwards;
  animation-delay: calc(var(--i, 0) * 45ms);
}
.figures .label { display: block; margin-bottom: 5px; }
.figures b {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: var(--tracking-display);
  color: var(--fg);
}
.figures small { display: block; margin-top: 5px; font-size: var(--text-xs); color: var(--muted); }
.figures li.is-lead {
  border-color: color-mix(in oklab, var(--accent) 42%, var(--border));
  background: var(--accent-soft);
}
.figures li.is-lead b { font-size: var(--text-2xl); }
.figures .row { margin-bottom: var(--s3); }
.figures .row .input { font-size: var(--text-sm); }

/* proportion bar: principal against interest */
.split {
  display: flex;
  height: 10px;
  border-radius: var(--r-full);
  overflow: hidden;
  background: var(--border);
}
.split i { height: 100%; transition: width var(--dur-2) var(--ease); }
.split i:first-child { background: var(--accent); }
.split i:last-child { background: var(--safe); }
.split-key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s5);
  margin-top: var(--s2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted);
}
.split-key span::before {
  content: "";
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: 7px;
  border-radius: 2px;
  background: var(--accent);
}
.split-key span + span::before { background: var(--safe); }
.split-key b { color: var(--fg); font-weight: 600; }

/* a segment with more than three options has to be allowed to wrap */
.segment-wrap { flex-wrap: wrap; }

/* the year by year schedule: sticky header, scrolls inside its own box */
.tbl-wrap {
  max-height: 340px;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg-tint);
}
.tbl { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: var(--text-xs); }
.tbl th, .tbl td {
  padding: 9px var(--s3);
  text-align: right;
  white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
.tbl th:first-child, .tbl td:first-child { text-align: left; }
.tbl thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-2);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.tbl tbody th { font-weight: 600; color: var(--fg-2); }
.tbl tfoot td, .tbl tfoot th {
  position: sticky;
  bottom: 0;
  background: var(--surface-2);
  color: var(--fg);
  font-weight: 600;
  border-bottom: 0;
}

/* --- 25. Lorem ipsum ----------------------------------------------------- */

/* Prose has to wrap, unlike code: `.result pre` is `white-space: pre`, so a
   paragraph ran off the right edge. The `pre.wrap` above adds
   `word-break: break-all`, which splits words mid letter and reads badly for
   text, so this is its prose sibling. */
.result pre.wrap-prose {
  white-space: pre-wrap;
  word-break: normal;
  overflow-wrap: anywhere;
  line-height: 1.7;
}

/* --- 26. Meta tag generator ---------------------------------------------- */

/* a search result, near enough: the point is seeing where the truncation
   falls, so the order and the shapes matter more than matching Google */
.serp {
  max-width: 600px;
  padding: var(--s4);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
}
.serp p { margin: 0; }
.serp-url { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--muted); }
.serp-title {
  margin-top: 5px;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: #1a0dab;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.serp-desc { margin-top: 5px; font-size: var(--text-sm); color: var(--fg-2); }
html[data-theme="dark"] .serp-title { color: #8ab4f8; }

/* --- 27. QR code generator ------------------------------------------------ */

/* the form on the left, the code on the right, stacked on a narrow screen */
.qr-wrap { display: grid; gap: var(--s5); align-items: start; }
@container col (min-width: 720px) {
  .qr-wrap { grid-template-columns: minmax(0, 1fr) 360px; }
}
.qr-form { display: grid; gap: var(--s4); align-content: start; }
.qr-out { display: grid; gap: var(--s3); justify-items: center; }

/* white behind the code at every theme: a scanner needs the contrast, and an
   inverted QR is a classic way to make one that will not read */
.qr-code {
  display: grid;
  place-items: center;
  width: 100%;
  padding: var(--s4);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: #ffffff;
}
.qr-code svg { display: block; width: 100%; height: auto; max-width: 340px; }

/* --- 28. Barcode generator ----------------------------------------------- */

/* a barcode is black on white or it is not a barcode, so the preview keeps its
   own white card in dark mode rather than inheriting the surface. The SVG
   carries its own white background and quiet zones; this pane is the padding
   around it. */
.barcode-pane {
  display: grid;
  place-items: center;
  min-height: 150px;
  padding: var(--s5);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: #fff;
  overflow: auto;
}
.barcode-pane svg { display: block; max-width: 100%; height: auto; }
.barcode-pane:empty::before {
  content: attr(data-empty);
  color: var(--faint);
  font-size: var(--text-sm);
}

/* --- 29. PDF tools ------------------------------------------------------- */

/* the ordered list of files waiting to be merged */
.filelist { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s2); }
.filelist li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s2) var(--s2) var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--bg-tint);
  animation: rise var(--dur-3) var(--ease) backwards;
  animation-delay: calc(var(--i, 0) * 35ms);
}
.file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-sm); }
.file-meta { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--muted); white-space: nowrap; }
.file-actions { display: flex; gap: 2px; }
@container col (max-width: 520px) {
  .filelist li { grid-template-columns: auto minmax(0, 1fr) auto; }
  .filelist li:has(.file-thumb) { grid-template-columns: auto 32px minmax(0, 1fr) auto; }
  .file-meta { grid-column: 2 / -1; }
}

.drop-hint { margin-top: var(--s2); font-size: var(--text-xs); color: var(--muted); }

/* a thumbnail in the image to PDF list, so a file is recognisable by sight */
.filelist li:has(.file-thumb) { grid-template-columns: auto 40px minmax(0, 1fr) auto auto; }
.file-thumb {
  width: 40px;
  height: 40px;
  border-radius: 4px;
  object-fit: cover;
  background: var(--surface-2);
}

/* --- 30. Image tools ----------------------------------------------------- */

/* the controls row: two number boxes and a lock side by side on a wide
   column, stacked when the sidebar squeezes it */
.img-form { display: grid; gap: var(--s4); align-content: start; }
@container col (min-width: 640px) {
  .img-form { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
}

/* before and after, side by side where there is room */
.img-pair { display: grid; gap: var(--s4); }
@container col (min-width: 640px) { .img-pair { grid-template-columns: 1fr 1fr; } }
.img-pane { margin: 0; display: grid; gap: var(--s2); min-width: 0; }

/* a checkerboard, because a transparent PNG on a flat surface looks like a
   white PNG, and the whole point of the JPEG warning is that it is not */
.img-box {
  display: grid;
  place-items: center;
  min-height: 160px;
  max-height: 460px;
  padding: var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  overflow: hidden;
  background-color: var(--surface-2);
  background-image:
    linear-gradient(45deg, rgba(128, 128, 128, 0.14) 25%, transparent 25% 75%, rgba(128, 128, 128, 0.14) 75%),
    linear-gradient(45deg, rgba(128, 128, 128, 0.14) 25%, transparent 25% 75%, rgba(128, 128, 128, 0.14) 75%);
  background-size: 18px 18px;
  background-position: 0 0, 9px 9px;
}
.img-box img { display: block; max-width: 100%; max-height: 420px; width: auto; height: auto; }
.img-pane figcaption {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted);
  text-align: center;
}

/* a colour swatch, not a full width text box */
.input-color { width: 58px; height: var(--ctl-h); padding: 4px; cursor: pointer; }

/* --- 31. Dragging a list into order -------------------------------------- */

/* The grip is a real button so it can be focused and used with the arrow
   keys. touch-action none is what stops a finger drag scrolling the page
   instead of moving the row. */
.grip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 6px 4px;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  /* muted rather than faint: the handle is the only hint that a row can be
     moved, so it has to be noticed without shouting */
  color: var(--muted);
  cursor: grab;
  touch-action: none;
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.grip:hover { color: var(--accent); background: var(--surface-2); }
/* a hint that the whole row is draggable, not only the six dots */
.filelist li:hover .grip { color: var(--fg-2); }
.grip:active { cursor: grabbing; }
.grip-num {
  min-width: 14px;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  text-align: right;
}

/* The row being dragged follows the finger with no transition of its own, so
   it cannot lag behind the pointer. The rows it displaces do transition, and
   that is the whole animation. */
.filelist li { transition: transform var(--dur-2) var(--ease), box-shadow var(--dur-1) var(--ease); }
.filelist li.is-dragging {
  position: relative;
  z-index: 3;
  transition: none;
  box-shadow: var(--shadow-pop);
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
  background: var(--surface);
  cursor: grabbing;
}
.filelist li.is-shifting { transition: transform var(--dur-2) var(--ease); }
/* the entry animation also drives transform, so it must not fight a drag */
.filelist.is-sorting li { animation: none; }
/* no text selection mid drag, which otherwise highlights every file name */
.filelist.is-sorting { user-select: none; }

.grip:focus-visible { background: var(--accent-soft); color: var(--fg); }

/* --- 32. Hashes and JWT claims -------------------------------------------- */

/* A hash must be shown whole. The case converter's rows cut their preview off
   with an ellipsis, which is right for a paragraph and wrong for a checksum:
   the entire reason the value is on screen is so it can be compared against
   another one by eye, and a SHA-512 ending in three dots cannot be. */
.hashes .case-out {
  white-space: normal;
  overflow-wrap: anywhere;
}

/* --- 32b. JWT claims ------------------------------------------------------ */

/* The same rows as the case converter, but the plain English note goes under
   the value rather than in a third column: the notes are whole sentences and
   a third column squeezed them to a few characters wide. */
.claims li { grid-template-columns: 92px minmax(0, 1fr); }
.claims .case-out { white-space: normal; overflow-wrap: anywhere; }
.claim-note {
  grid-column: 2;
  font-size: var(--text-xs);
  color: var(--muted);
}
.claim-note:empty { display: none; }

/* --- 33. Text diff -------------------------------------------------------- */

/* Fixed layout so the two sides stay the same width whatever is in them: with
   automatic layout one long line on the left shrank the right column to a
   sliver and the comparison stopped being a comparison. */
.diff { table-layout: fixed; }
.diff td {
  padding: 2px var(--s2);
  text-align: left;
  vertical-align: top;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  border-bottom: 0;
}
.d-num,
.d-sign {
  width: 46px;
  text-align: right;
  color: var(--faint);
  user-select: none; /* so selecting the diff copies the text, not the numbers */
}
.d-sign { width: 20px; text-align: center; color: var(--muted); }
.diff-unified .d-num { width: 42px; }

/* Colour is never the only signal: the unified view has its plus and minus
   column, and the split view says which side a row is on. */
.diff-split .d-del td:nth-child(2),
.diff-split .d-change td:nth-child(2) {
  background: color-mix(in oklab, var(--danger) 13%, transparent);
}
.diff-split .d-add td:nth-child(4),
.diff-split .d-change td:nth-child(4) {
  background: color-mix(in oklab, var(--safe) 15%, transparent);
}
.diff-unified .d-del { background: color-mix(in oklab, var(--danger) 13%, transparent); }
.diff-unified .d-add { background: color-mix(in oklab, var(--safe) 15%, transparent); }

/* the words that actually moved inside a line that was edited */
.diff ins,
.diff del {
  text-decoration: none;
  border-radius: 3px;
  padding: 0 1px;
}
.diff ins { background: color-mix(in oklab, var(--safe) 34%, transparent); }
.diff del { background: color-mix(in oklab, var(--danger) 30%, transparent); }

/* one column per side is unreadable on a phone, so the split view folds the
   right hand side onto its own row under the left */
@container col (max-width: 620px) {
  .diff-split,
  .diff-split tbody,
  .diff-split tr { display: block; }
  .diff-split td { display: inline-block; }
  .diff-split .d-text { width: calc(100% - 60px); }
  /* Stacked, a line with no counterpart would otherwise leave an empty row
     under it, which reads as a stray gap rather than as an absence. In the
     table layout that blank is what keeps the two sides aligned, so it can
     only be dropped here. */
  .diff-split .d-add td:nth-child(1),
  .diff-split .d-add td:nth-child(2),
  .diff-split .d-del td:nth-child(3),
  .diff-split .d-del td:nth-child(4) {
    display: none;
  }
}

/* --- 34. Cropping an image ------------------------------------------------ */

/* The stage carries padding so a handle sitting on the very edge of the
   picture still has somewhere to be: with the handles centred on the boundary,
   half of each one would otherwise fall outside and be clipped away, and a
   full frame selection would look like it had no handles at all. */
.crop-stage {
  position: relative;
  /* Shrink to the picture. Left as a full width block, the shade and the
     checkerboard carried on across the empty column beside a portrait or a
     small image, which looks like part of the photograph is missing. */
  width: fit-content;
  max-width: 100%;
  padding: 11px;
  overflow: hidden;
  border-radius: var(--r-sm);
  background-color: var(--surface-2);
  /* the same checkerboard as the result box, so a transparent PNG reads as
     transparent while you are cropping it and not only afterwards */
  background-image:
    linear-gradient(45deg, rgba(128, 128, 128, 0.14) 25%, transparent 25% 75%, rgba(128, 128, 128, 0.14) 75%),
    linear-gradient(45deg, rgba(128, 128, 128, 0.14) 25%, transparent 25% 75%, rgba(128, 128, 128, 0.14) 75%);
  background-size: 18px 18px;
  background-position: 0 0, 9px 9px;
}
/* The frame shrink wraps the picture, because the crop box is positioned
   against it: if the frame were full width, a small image would sit in a
   corner of it and the overlay would float somewhere off the photograph.
   `max-width` rather than `width` for the same reason a thumbnail should not
   be blown up to fill the column: a 60 pixel icon displayed a thousand wide
   is unusable to crop. The height cap keeps the whole stage, handles and all,
   inside a normal window without scrolling. */
.crop-frame {
  position: relative;
  display: inline-block;
  max-width: 100%;
  line-height: 0;
}
.crop-frame img {
  display: block;
  max-width: 100%;
  max-height: 60vh;
  width: auto;
  height: auto;
}

/* One enormous spread shadow darkens everything outside the selection, which
   is one element instead of the four boxes the obvious approach needs. The
   stage clips it, which is the only reason `overflow: hidden` is up there. */
.crop-box {
  position: absolute;
  box-sizing: border-box;
  border: 1px solid #fff;
  box-shadow: 0 0 0 9999px rgb(0 0 0 / 0.55);
  cursor: move;
  touch-action: none; /* or a drag on a phone scrolls the page instead */
}
.crop-box:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* no transition while dragging: the box must sit under the finger exactly */
.crop-box.is-dragging { cursor: grabbing; }

.crop-handle {
  position: absolute;
  width: 14px;
  height: 14px;
  background: var(--accent);
  border: 1px solid var(--accent-on);
  border-radius: 3px;
  transform: translate(-50%, -50%);
}
.crop-handle[data-h="nw"] { left: 0; top: 0; cursor: nwse-resize; }
.crop-handle[data-h="n"] { left: 50%; top: 0; cursor: ns-resize; }
.crop-handle[data-h="ne"] { left: 100%; top: 0; cursor: nesw-resize; }
.crop-handle[data-h="e"] { left: 100%; top: 50%; cursor: ew-resize; }
.crop-handle[data-h="se"] { left: 100%; top: 100%; cursor: nwse-resize; }
.crop-handle[data-h="s"] { left: 50%; top: 100%; cursor: ns-resize; }
.crop-handle[data-h="sw"] { left: 0; top: 100%; cursor: nesw-resize; }
.crop-handle[data-h="w"] { left: 0; top: 50%; cursor: ew-resize; }

/* A finger is about nine millimetres wide and a 14px square is not, so the
   grab area is widened past what is drawn rather than drawing something
   clumsy. */
.crop-handle::after {
  content: "";
  position: absolute;
  inset: -9px;
}

/* --- 35. Lining a bare checkbox up with labelled controls ----------------- */

/* A field that is only a checkbox has no label row above it. Dropped into a
   grid beside inputs and selects it therefore floated to the top of its cell
   while every real control sat at the bottom, which is what made "Convert to
   greyscale" and "Keep the aspect ratio" look like mistakes. Two rules fix
   every instance: align each row by its controls, and give a bare checkbox
   the height of a control so the two share a baseline. */
.calc-grid,
.img-form {
  align-items: end;
}
/* A range is about half the height of a text input, so bottom aligning it
   left the track floating above the inputs beside it. Give it the control
   height and the track centres itself inside. */
.calc-grid input[type="range"],
.img-form input[type="range"] {
  height: var(--ctl-h);
}
.calc-grid > .check,
.img-form > .check,
.calc-grid > .field > .check:only-child,
.img-form > .field > .check:only-child {
  min-height: var(--ctl-h);
}

/* --- 36. A result block that is not three elements jammed together -------- */

/* The crop result was a preview, a row of figures and a download button with
   no spacing between them, so the button read as an orphan rather than as the
   end of the sequence. Left aligned, because a full width download button
   looks like a mistake on a wide screen. */
.stack-start {
  display: grid;
  gap: var(--s4);
  justify-items: start;
}
.stack-start > * { max-width: 100%; }

/* --- 37. Picking one row out of a batch ---------------------------------- */

/* A list of results needs to say which one the big preview is showing, or
   clicking a row appears to do nothing. Marked on the row rather than with a
   separate control, because the row is what you clicked. */
.filelist li.is-current {
  border-color: color-mix(in oklab, var(--accent) 50%, var(--border));
  background: var(--accent-soft);
}
.filelist li.is-current .file-meta { color: var(--fg-2); }
/* only clickable where clicking does something */
.filelist li.is-current,
.filelist li:has(.file-thumb) { cursor: pointer; }
.filelist li:has(.file-thumb):hover { border-color: color-mix(in oklab, var(--accent) 34%, var(--border)); }

/* --- 38. Passport photo: the head guide ---------------------------------- */

/* An oval where the face is supposed to sit. Every authority publishes a head
   height rule and almost nobody reads it, so the rule is drawn instead: line
   the chin and the crown up with the oval and the photo passes. Sized by the
   script from the chosen country's own fraction, so this is only the look. */
.pp-guide {
  position: absolute;
  border: 2px dashed color-mix(in oklab, var(--accent) 80%, transparent);
  border-radius: 50%;
  pointer-events: none;
}
.pp-guide::after {
  content: "";
  position: absolute;
  left: -14%;
  right: -14%;
  bottom: 0;
  border-bottom: 1px dashed color-mix(in oklab, var(--accent) 55%, transparent);
}

/* the two results sit side by side where there is room: one photo, one sheet */
.pp-out { display: grid; gap: var(--s4); }
@container col (min-width: 640px) { .pp-out { grid-template-columns: 1fr 1.4fr; } }
.pp-out .img-box { background-color: var(--surface-2); }

/* ==========================================================================
   WebTools design system: "Paper & Ink"
   37. The PDF page grid: thumbnails you click instead of page numbers you
   guess at. Used by the split tool; written to be reusable by any PDF tool
   that has to ask "which pages".
   Concatenated after widgets.css, so anything here may override it. Colours
   come from the custom properties only, so both themes follow for free.
   ========================================================================== */

/* the row of bulk actions above the grid. The count sits on the left as a
   label so the grid always says how many pages are chosen, which is the one
   number the plan line below repeats. */
.pgrid-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
}
.pgrid-bar .pgrid-count { margin-right: auto; }

/* auto-fill rather than a fixed column count, because this grid sits inside
   .tool-col and the sidebar takes 268px of the window away from it */
.pgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
  gap: var(--s3);
  /* a hundred page document must not push the Split button off the screen,
     so the grid scrolls inside itself rather than growing without limit */
  max-height: 540px;
  overflow-y: auto;
  padding: 2px;
}

.pcard {
  position: relative;
  display: grid;
  gap: var(--s2);
  justify-items: center;
  padding: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1.15;
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.pcard:hover { border-color: var(--accent); transform: translateY(-1px); }
.pcard:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pcard.is-on { border-color: var(--accent); background: var(--accent-soft); color: var(--fg); }

/* In the two grouping modes the thumbnails are a picture of the result, not
   a set of switches, so they must not invite a click that does nothing. */
.pgrid[data-pick="off"] .pcard { cursor: default; transform: none; }
.pgrid[data-pick="off"] .pcard:hover { border-color: var(--border); }

/* the page itself. A fixed minimum height keeps the grid from reflowing as
   each thumbnail arrives, which on a long document is most of a second of
   the rows jumping around. */
.pcard-shot {
  display: grid;
  place-items: center;
  width: 100%;
  min-height: 96px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface-2);
  overflow: hidden;
}
.pcard-shot canvas { display: block; max-width: 100%; height: auto; }

.pcard-num { color: inherit; }

/* Every N pages: a band across the top plus a name, so the cut points are
   visible. Alternating two colours rather than one per group, because twenty
   groups would need twenty colours nobody could tell apart. An inset shadow
   and not a thicker border, or the contents shift by two pixels. */
.pcard.is-grouped { box-shadow: inset 0 3px 0 var(--accent); }
.pcard.is-alt { box-shadow: inset 0 3px 0 var(--muted); }
.pcard-tag {
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--muted);
}
.pcard-tag:empty { display: none; }

/* the tick, which is what makes "chosen" readable without relying on the
   tint alone */
.pcard-tick {
  position: absolute;
  top: 6px;
  right: 6px;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: var(--r-full);
  background: var(--accent);
  color: var(--accent-on);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity var(--dur-1) var(--ease), transform var(--dur-2) var(--ease-back);
}
.pcard.is-on .pcard-tick { opacity: 1; transform: scale(1); }

.pgrid-note { font-size: var(--text-xs); color: var(--muted); }

/* ==========================================================================
   WebTools design system: "Paper & Ink"
   38. Signing a PDF: the pad you sign on, and the box you drag onto a page.
   Concatenated after pdfgrid.css, so anything here may override it. Every
   colour comes from a custom property, so both themes follow for free. The
   handles are deliberately the crop tool's `.crop-handle`, positions and
   touch targets included: two identical sets of corner grips is one set too
   many, and the hand expects them to behave the same way.
   ========================================================================== */

/* the three ways of making a signature, one visible at a time */
.sig-panel {
  display: grid;
  gap: var(--s3);
}

/* This is the one place on the site that asks for a gesture rather than a
   value, so it has to read as somewhere you would sign: a wide pale field with
   a rule across it at the height a signature sits. `touch-action: none` is not
   optional, or the first stroke on a phone scrolls the page instead of drawing
   anything, and people conclude the tool is broken. */
.sig-pad {
  display: block;
  width: 100%;
  height: auto;
  border: 1px dashed var(--border);
  border-radius: var(--r-sm);
  background-color: var(--surface);
  background-image: linear-gradient(
    to bottom,
    transparent calc(78% - 1px),
    var(--border) calc(78% - 1px),
    var(--border) 78%,
    transparent 78%
  );
  cursor: crosshair;
  touch-action: none;
}

/* the photograph that was chosen, beside the button to choose another */
.sig-seen {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
}

/* Shared by the photograph and the finished signature. A minimum size so an
   empty box still reads as a place something will appear, rather than
   collapsing to nothing and making the caption look orphaned. */
.sig-shot {
  display: grid;
  place-items: center;
  min-width: 160px;
  min-height: 78px;
  padding: var(--s2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
}
/* Both axes capped and both dimensions auto. Setting only one and leaving the
   other to a max- rule is what once painted a square logo as an ellipse: each
   constraint clamped its own axis and the aspect ratio was lost. */
.sig-shot img {
  display: block;
  max-width: 100%;
  max-height: 110px;
  width: auto;
  height: auto;
}

.sig-preview {
  display: grid;
  justify-items: start;
  gap: var(--s2);
  margin: 0;
}
.sig-preview figcaption {
  font-size: var(--text-xs);
  color: var(--muted);
}

/* The page you are placing on. The stage shrinks to the page rather than
   filling the column, for the reason the crop stage does: as a full width
   block the mat carries on across the empty space beside a portrait page,
   which reads as part of the picture being missing. */
.sig-stage {
  width: fit-content;
  max-width: 100%;
  padding: var(--s4);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
/* The frame is exactly the canvas, because the boxes are positioned against
   it: if it were full width a narrow page would sit in one corner of it and
   every signature would float somewhere off the paper. */
.sig-frame {
  position: relative;
  display: block;
  line-height: 0;
}
.sig-frame canvas {
  display: block;
  max-width: 100%;
  height: auto;
  box-shadow: var(--shadow-2);
}

/* A dashed outline and a wash rather than a solid frame: the point of the box
   is to show where the signature will be, so it must never hide the signature
   or the text of the page underneath. Both go on saving; only the ink is
   drawn into the file. */
.sig-box {
  position: absolute;
  box-sizing: border-box;
  border: 1px dashed var(--accent);
  background: var(--accent-glow);
  cursor: move;
  touch-action: none;
}
.sig-box:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.sig-box img {
  display: block;
  width: 100%;
  height: 100%;
  /* or a drag that starts on the picture is a drag of the picture */
  pointer-events: none;
}

.sig-del {
  position: absolute;
  top: -11px;
  right: -11px;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--accent-on);
  border-radius: var(--r-full);
  background: var(--accent);
  color: var(--accent-on);
  cursor: pointer;
  line-height: 0;
}
.sig-del:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* the page open on the stage, marked in the grid as well as tinted */
.pcard.is-current {
  box-shadow: inset 0 3px 0 var(--accent);
}

/* ==========================================================================
   WebTools design system: "Paper & Ink"
   One widget: the EXIF viewer's "this photo knows where you were" banner.
   Split only to keep files small: the concatenation order is
   base, controls, home, shell, chrome, widgets, pdfgrid, exifgrid, and the
   cascade depends on this file coming after shell.css, which defines .badge.
   ========================================================================== */

/* --- 38. The location warning --------------------------------------------- */

/* Every tool page carries a green pill saying nothing leaves your browser.
   This is the same pill saying the opposite about the file you just dropped
   in, which keeps one vocabulary on the page for "here is a fact about your
   privacy" rather than inventing a second one. It has to shout, because a
   photograph that records the spot it was taken on is the single thing a
   visitor came to this page to find out.

   There is no --danger-soft token, so the tint is mixed from --danger against
   the surface: that follows the dark theme, where a fixed pale red would glow.
   Note that without this file the banner still renders as a perfectly legible
   badge in the wrong colour, which is the right way for a stylesheet to fail. */
.badge-alert {
  flex-wrap: wrap;
  max-width: 100%;
  padding: 6px 7px 6px 13px;
  border-color: color-mix(in oklab, var(--danger) 42%, var(--border));
  background: color-mix(in oklab, var(--danger) 11%, var(--surface));
  color: color-mix(in oklab, var(--danger), var(--fg) 24%);
}
.badge-alert .ico { color: var(--danger); }
/* The copy button sits inside the pill. Left alone it would inherit the pill's
   own red and read as part of the warning rather than as something to press. */
.badge-alert .btn { color: var(--fg); background: var(--surface); }
