/* ==========================================================================
   Pathine design system — "Index"
   Swiss wayfinding: hairline rules, numbered indices, extreme type contrast.
   Hand-authored CSS. The marketing + tool surface deliberately does not load
   Tailwind or DaisyUI — the whole system is ~20KB and blocks nothing.

   ORDERING RULE — a modifier must be declared after the base it modifies.

   `.input` and `.input-narrow` are both one class, so they have identical
   specificity and the later declaration wins. Put the modifier first and it
   silently loses, which is not a subtle failure: it produced 112px-wide text
   fields where full-width ones were asked for, and a form label stacked above
   its control instead of sitting beside it. Both shipped.

   So: keep a base and its modifiers together, base first. Where that is
   genuinely impractical, qualify the modifier — `.field.field-inline` — so it
   wins on specificity and stops caring about position.

   tests/test_stylesheet.py enforces this against the classes that actually
   appear together on an element in the templates, so a new modifier dropped
   into the wrong section fails in CI rather than on the page.
   ========================================================================== */

/* --- Typeface: Switzer, self-hosted (Fontshare) ---------------------------
   The @font-face rules are NOT here — they are inlined in website/base.html so
   their `src` can carry `?v=<hash>`, which nothing in a .css file can. They
   lived here and asked for the fonts with no query, while base.html preloaded
   the hashed URL: two different cache keys, so every first view downloaded all
   four faces twice and used the preload for none of them. Adding a weight means
   editing base.html and its preload together. See CLAUDE.md.
   -------------------------------------------------------------------------- */

/* --- Tokens --------------------------------------------------------------- */

:root {
  /* Surfaces */
  --paper: #FAFAF8;
  --paper-alt: #F1F1EC;
  --paper-raise: #FFFFFF;

  /* Ink — ink-3 is the lightest tone allowed to carry text (4.9:1 on paper);
     ink-4 is for hairline-adjacent marks: arrows, dots, inactive glyphs. */
  --ink: #17181A;
  --ink-2: #4A4D51;
  --ink-3: #6A6E73;
  --ink-4: #8A8E93;

  /* Rules — the structural device of the whole system */
  --rule: #E4E3DC;
  --rule-strong: #D3D2C9;
  --rule-ink: #C6C5BB;

  /* Accent */
  --accent: #0B6E75;
  --accent-deep: #08565C;
  --accent-tint: #E8F0F0;

  /* Flag — a requirement that was not met. Borrowed from the PDF category
     identity rather than a new red, so a failed row reads as part of the
     system and not as an alert someone bolted on. 6.6:1 on paper. */
  --flag: #9E4238;

  /* Category identities — desaturated, used at 1–2% of the page */
  --c-images: #8A5A44;
  --c-pdf: #9E4238;
  --c-video: #4C5E8A;
  --c-writing: #6B6340;
  --c-ai: #0B6E75;
  --c-finance: #3E6B4F;
  --c-calculators: #55606B;
  --c-converters: #3F6E86;
  --c-developer: #3D4F4A;
  --c-business: #7A6A45;

  /* Type scale — 11px labels against 76px display */
  --t-micro: 0.6875rem;   /* 11px */
  --t-small: 0.8125rem;   /* 13px */
  --t-body-s: 0.9375rem;  /* 15px */
  --t-body: 1.0625rem;    /* 17px */
  --t-lead: 1.1875rem;    /* 19px */
  --t-h3: 1.375rem;       /* 22px */
  --t-h2: clamp(1.875rem, 3.4vw, 2.625rem);
  --t-display: clamp(2.75rem, 7.6vw, 4.75rem);

  --track-micro: 0.14em;
  --track-display: -0.028em;

  /* Space — 8px base */
  --s-1: 0.5rem;
  --s-2: 1rem;
  --s-3: 1.5rem;
  --s-4: 2rem;
  --s-5: 3rem;
  --s-6: 4rem;
  --s-7: 6rem;
  --s-8: 8rem;

  /* Geometry */
  --r-card: 18px;
  --r-control: 14px;
  --r-chip: 999px;
  --gutter: 1.5rem;
  --page: 1240px;

  /* Elevation — barely there, on purpose */
  --lift: 0 1px 2px rgba(23, 24, 26, 0.03), 0 8px 24px -12px rgba(23, 24, 26, 0.10);
  --lift-strong: 0 1px 2px rgba(23, 24, 26, 0.04), 0 20px 48px -20px rgba(23, 24, 26, 0.16);

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* --- Reset ---------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: 'Switzer', ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: var(--t-body);
  line-height: 1.6;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: 'ss01' 1, 'cv01' 1;
}

h1, h2, h3, h4, p, figure, ul, ol { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg, canvas { display: block; max-width: 100%; }
button, input, textarea, select { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }

a {
  color: inherit;
  text-decoration: none;
}

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

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

/* --- Type primitives ------------------------------------------------------ */

.micro {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--ink-3);
  line-height: 1;
}

.micro-ink { color: var(--ink); }
.micro-accent { color: var(--accent); }

.display {
  font-size: var(--t-display);
  font-weight: 600;
  letter-spacing: var(--track-display);
  line-height: 0.98;
}

.h2 {
  font-size: var(--t-h2);
  font-weight: 600;
  letter-spacing: -0.022em;
  line-height: 1.08;
}

.h3 {
  font-size: var(--t-h3);
  font-weight: 600;
  letter-spacing: -0.014em;
  line-height: 1.24;
}

.lead {
  font-size: var(--t-lead);
  color: var(--ink-2);
  line-height: 1.55;
  letter-spacing: -0.008em;
}

.prose-note {
  font-size: var(--t-body-s);
  color: var(--ink-3);
  line-height: 1.6;
}

/* Index number — the recurring wayfinding motif */
.idx {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* --- Layout --------------------------------------------------------------- */

.page {
  width: 100%;
  max-width: var(--page);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

.rule {
  height: 1px;
  background: var(--rule);
  border: 0;
  margin: 0;
}

.band {
  border-top: 1px solid var(--rule);
}

.band-alt {
  background: var(--paper-alt);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

.section {
  padding-block: clamp(3.5rem, 8vw, 6.5rem);
}

/* Ruled section header: label · title · meta, like a printed table head */
.section-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--s-3);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--s-5);
}

.section-head .micro { margin-bottom: 0.9rem; display: block; }
.section-head-meta {
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-3);
  white-space: nowrap;
  padding-bottom: 0.35rem;
}
.section-head-meta a:hover { color: var(--accent); }

/* --- Header --------------------------------------------------------------- */

.masthead {
  position: sticky;
  top: 0;
  z-index: 60;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: saturate(140%) blur(12px);
  border-bottom: 1px solid var(--rule);
}

.masthead-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  height: 62px;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  font-weight: 600;
  font-size: 1.0625rem;
  letter-spacing: -0.022em;
}

.wordmark-glyph {
  width: 20px;
  height: 20px;
  flex: none;
  color: var(--accent);
}

.nav {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin-left: auto;
}

.nav a {
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink-2);
  letter-spacing: -0.005em;
  position: relative;
  padding-block: 0.4rem;
  transition: color 0.18s var(--ease);
}

.nav a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.24s var(--ease);
}

.nav a:hover { color: var(--ink); }
.nav a:hover::after { transform: scaleX(1); }

.nav-new::before {
  content: '';
  display: inline-block;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
  margin-right: 0.4rem;
  vertical-align: 1.5px;
}

.masthead-search {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  color: var(--ink-2);
  border: 1px solid transparent;
  transition: background 0.18s var(--ease), border-color 0.18s var(--ease), color 0.18s var(--ease);
}

.masthead-search:hover {
  background: var(--paper-alt);
  border-color: var(--rule);
  color: var(--ink);
}

/* --- Mobile menu ----------------------------------------------------------
   Below 720px `.nav` is gone, and until this existed nothing replaced it: a
   phone could reach the homepage, search and its account, and had no way at
   all to reach a category. The trigger is hidden above that breakpoint, where
   the nav is back and a hamburger beside it would be two doors to one room.
   -------------------------------------------------------------------------- */

.masthead-menu {
  display: none;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  color: var(--ink-2);
  border: 1px solid transparent;
  transition: background 0.18s var(--ease), border-color 0.18s var(--ease), color 0.18s var(--ease);
}

.masthead-menu:hover {
  background: var(--paper-alt);
  border-color: var(--rule);
  color: var(--ink);
}

/* Scroll behind an open modal is the browser's default and is wrong here: the
   panel is the whole screen's worth of interface, and the page sliding around
   under a finger aimed at it reads as breakage. */
.has-menu-open { overflow: hidden; }

.menu {
  /* A dialog is centred by the UA and capped at `calc(100% - 6px - 2em)`.
     Both have to be undone to hang it off the right edge full-height. */
  margin: 0 0 0 auto;
  width: min(21rem, 88vw);
  max-width: none;
  height: 100dvh;
  max-height: 100dvh;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink);
  overflow: hidden;
  transform: translateX(100%);
  /* `display` and `overlay` are discrete properties: without allow-discrete
     the panel is removed from the top layer the instant close() is called and
     the exit transition never runs. Browsers without them simply snap. */
  transition:
    transform 0.26s var(--ease),
    overlay 0.26s var(--ease) allow-discrete,
    display 0.26s var(--ease) allow-discrete;
}

.menu[open] { transform: none; }

@starting-style {
  .menu[open] { transform: translateX(100%); }
}

.menu::backdrop {
  background: color-mix(in srgb, var(--ink) 26%, transparent);
  opacity: 0;
  transition:
    opacity 0.26s var(--ease),
    overlay 0.26s var(--ease) allow-discrete,
    display 0.26s var(--ease) allow-discrete;
}

.menu[open]::backdrop { opacity: 1; }

@starting-style {
  .menu[open]::backdrop { opacity: 0; }
}

.menu-panel {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 var(--s-3) calc(var(--s-3) + env(safe-area-inset-bottom, 0px));
}

.menu-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: none;
  height: 62px;                 /* the masthead it sits over */
  margin-bottom: var(--s-2);
  border-bottom: 1px solid var(--rule);
}

.menu-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  margin-right: -0.45rem;       /* optical: the glyph, not its box, aligns */
  border-radius: 10px;
  color: var(--ink-2);
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}

.menu-close:hover { background: var(--paper-alt); color: var(--ink); }

.menu-nav {
  display: flex;
  flex-direction: column;
}

.menu-nav-alt { margin-top: var(--s-3); }

.menu-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.8rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-body-s);
  font-weight: 500;
  color: var(--ink);
  transition: color 0.18s var(--ease);
}

.menu-row:hover { color: var(--accent); }
.menu-row[hidden] { display: none; }
.menu-row .idx { width: 1.35rem; flex: none; }
.menu-row .cat-dot { flex: none; }
.menu-row-name { flex: 1 1 auto; }

.menu-row-go {
  flex: none;
  color: var(--ink-4);
  transition: transform 0.22s var(--ease), color 0.18s var(--ease);
}

.menu-row:hover .menu-row-go { color: var(--accent); transform: translateX(3px); }

/* The second group carries no index, so its names are indented to land on the
   same line as the category names above rather than starting a second column
   of their own: 1.35rem of index, the dot, and the two gaps between. */
.menu-nav-alt .menu-row { padding-left: calc(1.35rem + 6px + 1.4rem); }

.menu-count {
  font-size: var(--t-micro);
  font-weight: 600;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.menu-foot {
  margin-top: auto;
  padding-top: var(--s-3);
}

.menu-account {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  height: 44px;
  padding-inline: 0.85rem;
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink-2);
  transition: color 0.18s var(--ease), background 0.18s var(--ease), border-color 0.18s var(--ease);
}

.menu-account:hover {
  color: var(--ink);
  background: var(--paper-alt);
  border-color: var(--rule-strong);
}

.menu-account svg { flex: none; color: var(--ink-4); }
.menu-account:hover svg { color: var(--accent); }

.menu-legal {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--s-2);
  margin-top: var(--s-2);
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
}

.menu-legal a { color: var(--ink-3); }
.menu-legal a:hover { color: var(--ink); }

/* --- Hero canvas ----------------------------------------------------------
   A high-key landscape sitting behind the top of the homepage: warm haze at
   the top, pale ridges and a faint trail low down. It runs full-bleed behind
   the translucent masthead and is masked away before the first section rule,
   so every card below still sits on clean paper. Nothing here may darken the
   band where the headline sits — the display type stays 17:1 on paper.
   -------------------------------------------------------------------------- */

.has-hero-canvas::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: -1;
  /* Height tracks the hero, not the source ratio, and the picture is anchored
     to its bottom edge: the ridges and the trail are the only content in the
     frame, so they have to land inside the hero rather than below it. */
  height: clamp(520px, 44vw, 760px);
  /* The URL comes from the template, because only a template can write the
     `?v=<hash>` — and the preload in index.html has to name the same URL or
     the picture downloads twice and the preload counts for nothing. Everything
     about *how* it sits stays here. A page that sets .has-hero-canvas without
     defining --hero-src gets no image at all; tests/test_static.py catches it. */
  background: var(--hero-src) center 42% / cover no-repeat;
  /* The source is already high-key, so this must not fade it a second time.
     Multiply means the photograph's near-white sky disappears into the paper
     entirely — no pale rectangle sitting over #FAFAF8 — and only the ridges
     and the trail actually mark the page. Full strength; the mask only
     feathers the bottom edge so it doesn't meet the paper as a hard line. */
  opacity: 1;
  mix-blend-mode: multiply;
  filter: saturate(1.08);
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 84%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 84%, transparent 100%);
}

@media (max-width: 900px) {
  .has-hero-canvas::before {
    height: 520px;
    background-position: 34% bottom;
  }
}

@media (max-width: 900px) {
  .has-hero-canvas::before { background-position: center 42%; }
}

/* --- Hero ----------------------------------------------------------------- */

.hero {
  padding-top: clamp(4rem, 11vw, 8.5rem);
  padding-bottom: clamp(3rem, 7vw, 5.5rem);
}

.hero-top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 232px;
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
}

.hero-index {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: var(--s-4);
}

/* Marginalia: three quiet entries, ruled like a printed index */
.hero-aside {
  border-top: 1px solid var(--rule-strong);
  padding-top: 0.85rem;
  margin-top: 0.2rem;
}

/* This column can sit over the densest part of the hero image — under the
   survey treatment its label measured 3.76:1 in --ink-3 — so it steps darker. */
.hero-aside .micro { color: var(--ink-2); }

.hero-aside-item {
  display: grid;
  grid-template-columns: 1.9rem minmax(0, 1fr) 14px;
  align-items: center;
  gap: 0.5rem;
  padding-block: 0.7rem;
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-small);
  font-weight: 500;
  letter-spacing: -0.008em;
  color: var(--ink-2);
  transition: color 0.18s var(--ease);
}

.hero-aside-item svg {
  width: 14px;
  height: 14px;
  color: var(--ink-4);
  opacity: 0;
  transition: opacity 0.18s var(--ease), color 0.18s var(--ease);
}

.hero-aside-item:hover { color: var(--ink); }
.hero-aside-item:hover svg { opacity: 1; color: var(--accent); }
.hero-aside-item:hover .idx { color: var(--accent); }

.hero-index .tick {
  width: 26px;
  height: 1px;
  background: var(--rule-ink);
}

.hero h1 {
  max-width: 15ch;
  margin-bottom: var(--s-3);
  text-wrap: balance;
}

.hero-lead {
  max-width: 42ch;
  text-wrap: balance;
  margin-bottom: clamp(2rem, 4vw, 2.75rem);
}

/* --- Search: the centerpiece ---------------------------------------------- */

.search {
  position: relative;
  max-width: 720px;
  z-index: 50; /* the results panel must cover the example chips below it */
}

.search-shell {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  background: var(--paper-raise);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-card);
  padding: 0.25rem 0.5rem 0.25rem 1.125rem;
  height: 68px;
  box-shadow: var(--lift);
  transition: border-color 0.2s var(--ease), box-shadow 0.25s var(--ease);
}

.search:focus-within .search-shell {
  border-color: var(--accent);
  box-shadow: var(--lift-strong), 0 0 0 3px var(--accent-tint);
}

.search-icon {
  width: 19px;
  height: 19px;
  flex: none;
  color: var(--ink-4);
  transition: color 0.2s var(--ease);
}

.search:focus-within .search-icon { color: var(--accent); }

.search-input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  outline: none;
  font-size: 1.125rem;
  letter-spacing: -0.012em;
  color: var(--ink);
  height: 100%;
}

.search-input::placeholder { color: var(--ink-3); }

.search-key {
  font-size: var(--t-micro);
  font-weight: 600;
  color: var(--ink-3);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 0.25rem 0.45rem;
  line-height: 1;
  background: var(--paper);
}

.search-go {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: var(--r-control);
  background: var(--ink);
  color: var(--paper);
  transition: background 0.2s var(--ease), transform 0.2s var(--ease);
}

.search-go:hover { background: var(--accent); }
.search-go:active { transform: scale(0.96); }
.search-go svg { width: 18px; height: 18px; }

/* Results panel — reads like a printed index */
.search-results {
  position: absolute;
  top: calc(100% + 0.5rem);
  left: 0;
  right: 0;
  background: var(--paper-raise);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-card);
  box-shadow: var(--lift-strong);
  overflow: hidden;
  z-index: 70;
  display: none;
}

.search-results[data-open='true'] { display: block; }

.search-results-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem 1.125rem;
  border-bottom: 1px solid var(--rule);
}

.search-result {
  display: grid;
  grid-template-columns: 2.5rem minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.8rem 1.125rem;
  border-bottom: 1px solid var(--rule);
  transition: background 0.14s var(--ease);
}

.search-result:last-child { border-bottom: 0; }
.search-result[aria-selected='true'],
.search-result:hover { background: var(--paper-alt); }

.search-result-body {
  display: block;
  min-width: 0;
  overflow: hidden;
}

.search-result-name {
  font-weight: 500;
  letter-spacing: -0.012em;
  font-size: var(--t-body-s);
}

.search-result-desc {
  display: block;
  font-size: var(--t-small);
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search-result-cat {
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-3);
}

.search-empty {
  padding: 1.25rem 1.125rem;
  font-size: var(--t-body-s);
  color: var(--ink-3);
}

/* Example searches */
.examples {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.25rem;
  max-width: 720px;
}

/* This label sits lowest in the hero, where the ridge is strongest — ink-3
   measures 4.35:1 there, so it steps one tone darker. */
.examples .micro { margin-right: 0.35rem; color: var(--ink-2); }

.chip {
  font-size: var(--t-small);
  color: var(--ink-2);
  border: 1px solid var(--rule);
  /* Faint paper backing so the chips keep their shape over the hero image */
  background: color-mix(in srgb, var(--paper) 72%, transparent);
  backdrop-filter: blur(3px);
  border-radius: var(--r-chip);
  padding: 0.4rem 0.85rem;
  line-height: 1.2;
  transition: border-color 0.18s var(--ease), color 0.18s var(--ease), background 0.18s var(--ease);
}

.chip:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-tint);
}

/* --- Tool cards ----------------------------------------------------------- */

.tool-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  overflow: hidden;
}

.tool-card {
  grid-column: span 2;
  background: var(--paper);
  padding: 1.375rem 1.375rem 1.125rem;
  display: flex;
  flex-direction: column;
  min-height: 178px;
  position: relative;
  transition: background 0.22s var(--ease);
}

.tool-card:hover { background: var(--paper-raise); }

.tool-card--3 { grid-column: span 3; }
.tool-card--4 {
  grid-column: span 4;
  min-height: 218px;
  padding: 1.75rem 1.75rem 1.25rem;
}

.tool-card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-2);
  margin-bottom: auto;
}

.tool-icon {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  border: 1px solid var(--rule-strong);
  display: grid;
  place-items: center;
  color: var(--cat, var(--ink-2));
  background: var(--paper-raise);
  transition: border-color 0.22s var(--ease), transform 0.3s var(--ease);
}

.tool-icon svg { width: 20px; height: 20px; }
.tool-card:hover .tool-icon { border-color: var(--cat, var(--accent)); }

.tool-card h3 {
  font-size: 1.0625rem;
  font-weight: 500;
  letter-spacing: -0.016em;
  margin-top: var(--s-3);
}

.tool-card--4 h3 { font-size: 1.375rem; }
.tool-card--4 p { font-size: var(--t-body); max-width: 40ch; }

.tool-card p {
  font-size: var(--t-body-s);
  color: var(--ink-3);
  margin-top: 0.35rem;
  max-width: 34ch;
  line-height: 1.5;
}

/* A hairline gives the empty space a job: it turns each card into a
   printed entry rather than a box with something floating in it. */
.tool-card-foot {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.125rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--rule);
}

.tool-card-arrow {
  margin-left: auto;
  width: 16px;
  height: 16px;
  color: var(--ink-4);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity 0.22s var(--ease), transform 0.22s var(--ease), color 0.22s var(--ease);
}

.tool-card:hover .tool-card-arrow {
  opacity: 1;
  transform: translateX(0);
  color: var(--cat, var(--accent));
}

/* accent hairline drawn along the top edge on hover */
.tool-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--cat, var(--accent));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.4s var(--ease);
}

.tool-card:hover::before { transform: scaleX(1); }

.cat-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--cat, var(--ink-4));
  flex: none;
}

/* --- Categories ----------------------------------------------------------- */

/* One column per category, set by the template from the catalog rather than
   fixed here. It was `repeat(5, 1fr)` — sized for the ten-category catalog this
   site launched with, two rows of five. Cutting to V1's four left a fifth track
   with nothing in it, which rendered as an empty grey block on the homepage
   that looked like a tool had failed to load. Driving it from the data means
   the row stays full whether there are three categories or six. */
.cat-grid {
  display: grid;
  grid-template-columns: repeat(var(--cat-cols, 4), 1fr);
  gap: 1px;
  background: var(--rule);
  border-block: 1px solid var(--rule);
}

.cat-cell {
  background: var(--paper);
  padding: var(--s-3) var(--s-2) var(--s-3) var(--s-2);
  min-height: 148px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  position: relative;
  transition: background 0.22s var(--ease);
}

.cat-cell:hover { background: var(--paper-raise); }

.cat-cell-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.cat-cell .idx { transition: color 0.22s var(--ease); }
.cat-cell:hover .idx { color: var(--cat); }

.cat-cell-name {
  font-size: 1.0625rem;
  font-weight: 500;
  letter-spacing: -0.014em;
}

.cat-cell-count {
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-3);
  margin-top: 0.3rem;
}

.cat-cell::after {
  content: '';
  position: absolute;
  left: var(--s-2);
  right: var(--s-2);
  bottom: 0;
  height: 2px;
  background: var(--cat);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s var(--ease);
}

.cat-cell:hover::after { transform: scaleX(1); }

/* --- Featured tool experience --------------------------------------------- */

.demo {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}

.demo-copy h2 { margin-bottom: var(--s-3); max-width: 14ch; }
.demo-copy .lead { max-width: 38ch; }

.demo-steps {
  margin-top: var(--s-4);
  border-top: 1px solid var(--rule);
}

.demo-step {
  display: grid;
  grid-template-columns: 2.25rem minmax(0, 1fr);
  gap: 0.75rem;
  padding-block: 0.9rem;
  border-bottom: 1px solid var(--rule);
  align-items: baseline;
  color: var(--ink-3);
  transition: color 0.3s var(--ease);
}

.demo-step[data-active='true'] { color: var(--ink); }
.demo-step[data-active='true'] .idx { color: var(--accent); }
.demo-step-label { font-size: var(--t-body-s); font-weight: 500; }

.demo-stage {
  background: var(--paper-raise);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-card);
  box-shadow: var(--lift);
  overflow: hidden;
}

.demo-stage-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.7rem 1rem;
  border-bottom: 1px solid var(--rule);
}

.demo-stage-body {
  position: relative;
  aspect-ratio: 16 / 10;
  display: grid;
  place-items: center;
  padding: var(--s-3);
  background:
    linear-gradient(var(--rule) 1px, transparent 1px) 0 0 / 100% 28px,
    linear-gradient(90deg, var(--rule) 1px, transparent 1px) 0 0 / 28px 100%,
    var(--paper);
  background-blend-mode: normal;
}

.demo-stage-body::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(120% 90% at 50% 40%, transparent 30%, var(--paper-raise) 100%);
  pointer-events: none;
}

.demo-frame {
  position: relative;
  z-index: 2;
  width: min(320px, 78%);
  border-radius: 14px;
  overflow: hidden;
  border: 1px solid var(--rule-strong);
  background: var(--paper-raise);
  box-shadow: var(--lift-strong);
}

.demo-figure {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--paper-alt);
}

.demo-photo {
  width: 100%;
  height: 100%;
  transition: transform 0.65s var(--ease);
}

/* ── Demo effects ────────────────────────────────────────────────────────
   Each demo declares one via data-effect; the JS only advances data-state.
   Adding a demo with a new effect means a rule here and nothing else. */

/* cutout — the backdrop lifts away, leaving the subject on the checkerboard. */
.demo-figure[data-effect='cutout'] {
  background:
    conic-gradient(from 90deg, #F1F1EC 0 25%, #FAFAF8 0 50%, #F1F1EC 0 75%, #FAFAF8 0)
    0 0 / 16px 16px;
}

.demo-photo-bg { transition: opacity 0.55s var(--ease); }

.demo-figure[data-effect='cutout'][data-state='done'] .demo-photo-bg { opacity: 0; }

/* resize — the picture steps down to the size that was asked for. */
.demo-figure[data-effect='resize'][data-state='done'] .demo-photo {
  transform: scale(0.55);
}

/* swatches — the palette rises over the photograph. */
.demo-swatches {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  opacity: 0;
  transform: translateY(100%);
  transition: opacity 0.4s var(--ease), transform 0.55s var(--ease);
}

.demo-swatches span { flex: 1; height: 34px; background: var(--s); }

.demo-figure[data-effect='swatches'][data-state='done'] .demo-swatches {
  opacity: 1;
  transform: none;
}

/* Only the compression demo tells its story with a size bar. */
.demo-bar { display: none; }
.demo-figure[data-effect='bar'] ~ .demo-bar { display: block; }

/* The picture is meant to look identical before and after — that is the point
   of compression — so the bar carries the story instead of the image. */
.demo-bar {
  height: 6px;
  margin: 0 var(--s-2);
  border-radius: 999px;
  background: var(--paper-alt);
  overflow: hidden;
}

.demo-bar-fill {
  display: block;
  height: 100%;
  width: 100%;
  border-radius: inherit;
  background: var(--accent);
  transition: width 0.7s var(--ease);
}

.demo-figure[data-state='working'] ~ .demo-bar .demo-bar-fill { width: 62%; }
.demo-figure[data-state='done'] ~ .demo-bar .demo-bar-fill { width: 13%; }


.demo-scan {
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--accent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 25%, transparent);
  opacity: 0;
  top: 0;
}

.demo-figure[data-state='working'] .demo-scan {
  opacity: 1;
  animation: scan 1.1s var(--ease) infinite;
}

@keyframes scan {
  0% { top: 0; }
  100% { top: 100%; }
}

.demo-caption {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.6rem 0.8rem;
  border-top: 1px solid var(--rule);
}

.demo-note {
  margin-top: var(--s-3);
  font-size: var(--t-body-s);
  color: var(--ink-3);
}

.demo-replay {
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--accent);
}

/* --- Principles ----------------------------------------------------------- */

.principles {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--rule);
}

.principle {
  padding: var(--s-4) var(--s-3) var(--s-4) 0;
  border-right: 1px solid var(--rule);
}

.principle:last-child { border-right: 0; }
.principle:not(:first-child) { padding-left: var(--s-3); }

.principle h3 {
  font-size: var(--t-h3);
  font-weight: 500;
  letter-spacing: -0.018em;
  margin-block: 0.75rem 0.4rem;
}

.principle p {
  font-size: var(--t-body-s);
  color: var(--ink-3);
  max-width: 30ch;
}

/* --- Collections ---------------------------------------------------------- */

.collections {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  overflow: hidden;
}

.collection {
  background: var(--paper);
  padding: var(--s-3);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  transition: background 0.2s var(--ease);
}

.collection:hover { background: var(--paper-raise); }

.collection-name {
  font-size: var(--t-body);
  font-weight: 500;
  letter-spacing: -0.014em;
}

.collection-meta {
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-3);
  margin-top: 0.25rem;
}

.collection svg {
  width: 16px;
  height: 16px;
  color: var(--ink-4);
  flex: none;
  transition: transform 0.24s var(--ease), color 0.24s var(--ease);
}

.collection:hover svg { transform: translateX(3px); color: var(--accent); }

/* --- Footer --------------------------------------------------------------- */

.footer {
  border-top: 1px solid var(--rule);
  background: var(--paper-alt);
  padding-block: var(--s-6) var(--s-4);
}

.footer-grid {
  display: grid;
  grid-template-columns: 1.4fr repeat(3, 1fr);
  gap: var(--s-4);
  padding-bottom: var(--s-5);
  border-bottom: 1px solid var(--rule-strong);
}

.footer-brand .wordmark { margin-bottom: 0.9rem; }

.footer-statement {
  font-size: var(--t-body-s);
  color: var(--ink-3);
  max-width: 28ch;
}

.footer-col h4 {
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: var(--s-2);
}

.footer-col li { margin-bottom: 0.55rem; }

.footer-col a {
  font-size: var(--t-body-s);
  color: var(--ink-3);
  transition: color 0.16s var(--ease);
}

.footer-col a:hover { color: var(--accent); }

.footer-base {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding-top: var(--s-3);
  flex-wrap: wrap;
}

/* Install. Sits in the footer rather than the masthead on purpose: it is an
   offer, not a step, and a banner over the tool someone came here to use costs
   more than it earns. `hidden` until the browser confirms it is installable. */
.install {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: var(--t-small);
  color: var(--ink-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-chip);
  padding: 0.35rem 0.8rem;
  line-height: 1.2;
  transition: border-color 0.18s var(--ease), color 0.18s var(--ease), background 0.18s var(--ease);
}

.install:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-tint);
}

.install[hidden] { display: none; }

/* Installed already: the offer is noise inside the app window. */
@media (display-mode: standalone) {
  .install { display: none; }
}

/* --- Tool page ------------------------------------------------------------ */

.crumbs {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-block: var(--s-3) 0;
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-4);
  flex-wrap: wrap;
}

.crumbs a { transition: color 0.16s var(--ease); }
.crumbs a:hover { color: var(--accent); }
.crumbs [aria-current] { color: var(--ink-2); }

.tool-head {
  padding-block: var(--s-4) var(--s-4);
}

.tool-head h1 {
  font-size: clamp(2.25rem, 5.2vw, 3.5rem);
  font-weight: 600;
  letter-spacing: -0.028em;
  line-height: 1.02;
  margin-bottom: 0.75rem;
}

.tool-head .lead { max-width: 52ch; }

.tool-head-meta {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
  flex-wrap: wrap;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-3);
  border: 1px solid var(--rule);
  border-radius: var(--r-chip);
  padding: 0.35rem 0.7rem;
  line-height: 1;
}

/* The utility itself — dominates the page */
.workbench {
  background: var(--paper-raise);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-card);
  box-shadow: var(--lift);
  overflow: hidden;
}

.workbench-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding: 0.8rem 1.125rem;
  border-bottom: 1px solid var(--rule);
  flex-wrap: wrap;
}

.dropzone {
  margin: var(--s-3);
  border: 1px dashed var(--rule-ink);
  border-radius: var(--r-control);
  padding: clamp(2rem, 6vw, 3.5rem) var(--s-3);
  text-align: center;
  background: var(--paper);
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease);
}

.dropzone[data-drag='true'] {
  border-color: var(--accent);
  background: var(--accent-tint);
}

.dropzone-icon {
  width: 34px;
  height: 34px;
  margin: 0 auto var(--s-2);
  color: var(--ink-4);
}

.dropzone h2 {
  font-size: 1.1875rem;
  font-weight: 500;
  letter-spacing: -0.016em;
  margin-bottom: 0.35rem;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  height: 44px;
  padding-inline: 1.25rem;
  border-radius: var(--r-control);
  background: var(--ink);
  color: var(--paper);
  font-size: var(--t-body-s);
  font-weight: 500;
  letter-spacing: -0.008em;
  transition: background 0.2s var(--ease), transform 0.16s var(--ease);
}

.btn:hover { background: var(--accent); }
.btn:active { transform: scale(0.985); }

.btn-quiet {
  background: transparent;
  color: var(--ink-2);
  border: 1px solid var(--rule-strong);
}

.btn-quiet:hover {
  background: var(--paper-alt);
  color: var(--ink);
  border-color: var(--rule-ink);
}

/* `max-width: 100%` from the reset makes a sprite <svg> collapse inside a
   shrink-to-fit track: the column is sized from its content, the icon's width
   is clamped against that, and the two resolve to something smaller than the
   icon. Opting icons out of the clamp fixes it everywhere at once, rather than
   one tool at a time. */
.btn svg { width: 16px; height: 16px; flex: none; max-width: none; }

/* ── Tool state machine ──────────────────────────────────────────────────
   Four states, one attribute: empty · loading · result · error. Tool scripts
   set root[data-state] and never show or hide anything themselves, which is
   what keeps every tool on the site behaving the same way. Adding a state
   here means adding it to STATES in static/js/runtime/state.js too. */

.workbench-result,
.workbench-loading,
.workbench-error { display: none; }

.workbench[data-state='result'] .workbench-result { display: block; }
.workbench[data-state='loading'] .workbench-loading { display: block; }
.workbench[data-state='error'] .workbench-error { display: block; }

.workbench[data-state='result'] .dropzone,
.workbench[data-state='loading'] .dropzone,
.workbench[data-state='error'] .dropzone { display: none; }

.workbench-loading,
.workbench-error {
  padding: var(--s-5) var(--s-3);
  text-align: center;
}

/* Re-running with new settings keeps the result on screen and marks the bar
   instead. Deliberately not an opacity change on the result: dimming the
   image is the same flicker in a quieter costume, and the point is that the
   thing you are looking at stays still while you drag a slider. */
.workbench-bar { position: relative; overflow: hidden; }

.workbench-bar::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  height: 2px;
  width: 40%;
  background: var(--accent);
  opacity: 0;
  transform: translateX(-100%);
}

.workbench[data-working='true'] .workbench-bar::after {
  opacity: 1;
  animation: sweep 1.05s var(--ease) infinite;
}

@keyframes sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(250%); }
}

@media (prefers-reduced-motion: reduce) {
  .workbench[data-working='true'] .workbench-bar::after {
    animation: none;
    width: 100%;
    transform: none;
    opacity: 0.35;
  }
}

.workbench-error h2 { font-size: var(--t-h3); letter-spacing: -0.02em; }
.workbench-error .dropzone-icon { color: var(--ink-3); }

.spinner {
  display: inline-block;
  width: 22px;
  height: 22px;
  margin-bottom: 0.9rem;
  border: 2px solid var(--rule-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

@keyframes spin { to { transform: rotate(1turn); } }

/* ── Batch results ──────────────────────────────────────────────────────
   One file gets the large before/after comparison; several get the list. */

.workbench[data-mode='batch'] .compare,
.workbench[data-mode='single'] .batch { display: none; }

.batch { border-bottom: 1px solid var(--rule); }

.batch-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: var(--s-3) var(--s-3) 0.75rem;
}

.batch-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 26rem;
  overflow-y: auto;
}

.batch-item {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: var(--s-2);
  padding: 0.7rem var(--s-3);
  border-top: 1px solid var(--rule);
}

.batch-item img {
  width: 40px;
  height: 40px;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--rule);
}

.batch-name {
  font-size: var(--t-body-s);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.batch-size {
  font-size: var(--t-small);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* On a phone the row becomes two lines rather than five columns: name and
   action on top, sizes underneath, thumbnail spanning both. Letting the
   columns wrap on their own put the download button on its own empty row. */
@media (max-width: 720px) {
  .batch-item {
    grid-template-columns: 32px minmax(0, 1fr) auto;
    grid-template-areas:
      "thumb name  action"
      "thumb size  delta";
    row-gap: 0.1rem;
  }
  .batch-item img { grid-area: thumb; width: 32px; height: 32px; align-self: center; }
  .batch-name { grid-area: name; }
  .batch-size { grid-area: size; }
  .batch-item .stat-delta { grid-area: delta; text-align: right; }
  .batch-item .btn { grid-area: action; padding: 0.5rem 0.7rem; }
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Editors ─────────────────────────────────────────────────────────────
   The surface for tools that take text rather than a file. Two panes on a
   desktop, stacked on a phone; one pane when there is no separate output. */

.editor {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--rule);
  border-bottom: 1px solid var(--rule);
}

.editor-single { grid-template-columns: 1fr; }

.editor-pane {
  display: flex;
  flex-direction: column;
  background: var(--paper);
  min-width: 0;
}

.editor-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding: 0.85rem var(--s-3) 0.6rem;
  min-height: 2.9rem;
}

.editor-actions { display: flex; gap: 0.4rem; }

.editor-area {
  flex: 1;
  min-height: 17rem;
  padding: 0 var(--s-3) var(--s-3);
  border: 0;
  background: transparent;
  resize: vertical;
  color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13.5px;
  line-height: 1.65;
  tab-size: 2;
}

/* Prose, not code — the word counter is for writing, so it gets the body face. */
.editor-prose {
  font-family: inherit;
  font-size: var(--t-body-s);
  line-height: 1.7;
}

.editor-area:focus-visible { outline: none; }
.editor-area::placeholder { color: var(--ink-4); }
.editor-area[readonly] { color: var(--ink-2); }

/* A single band under the editors, always the same height.
   A document is invalid for most of the time you are typing it, so a strip
   that expands and collapses on the way to valid JSON shunts the controls up
   and down on nearly every keystroke. Reserving the space fixes that — and
   rather than leaving it blank when the document parses, the same band
   reports what was parsed. Only the colour changes. */
.editor-status {
  position: relative;
  padding: 0.7rem var(--s-3);
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-small);
  line-height: 1.4;
  /* Two clamped lines plus the padding, so a one-line message and a two-line
     one occupy exactly the same box. */
  min-height: calc(2.8em + 1.4rem);
  transition: background 0.2s var(--ease);
}

.workbench[data-valid='false'] .editor-status { background: #FEF6F2; }

.editor-stats,
.editor-error {
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.editor-stats { color: var(--ink-2); }

.editor-error {
  color: #9A3412;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* Exactly one of the two is shown, and the band's height does not depend on
   which. */
.editor-error { display: none; }
.workbench[data-valid='false'] .editor-stats { display: none; }
.workbench[data-valid='false'] .editor-error {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* `display: inline-flex` above beats the user agent's `[hidden]`, so without
   this every button carrying the attribute renders anyway. Nothing on the site
   shipped a hidden `.btn` until Chat Catch-Up, which is why it went unnoticed:
   "Use a theme instead" sat there before any art existed, and on a browser
   without `navigator.canShare` the Share button would have shown and done
   nothing. Same trap as `.shot-strip`/`.card-art`, and this one is on the base
   component so it covers every button rather than the three that exposed it. */
.btn[hidden] { display: none; }

.btn-small { padding: 0.38rem 0.65rem; font-size: var(--t-small); }
.btn-quiet[data-copied='true'] { border-color: var(--accent); color: var(--accent); }

/* Qualified with .field so it beats the base rule whichever order they end up
   in. Unqualified, `.field { flex-direction: column }` won on source order and
   stacked the label above the control, leaving it taller than everything
   beside it in the row. */
.field.field-inline {
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
}

/* ── Verdict + code context (JSON Validator) ───────────────────────────── */

.verdict { padding: var(--s-3); border-bottom: 1px solid var(--rule); }

.verdict-line {
  margin: 0;
  font-size: var(--t-h3);
  letter-spacing: -0.02em;
}

.workbench[data-valid='true'] .verdict-line { color: var(--accent); }
.workbench[data-valid='false'] .verdict-line { color: #9A3412; }

.verdict-detail {
  margin: 0.3rem 0 0;
  font-size: var(--t-body-s);
  color: var(--ink-2);
}

.code-block {
  margin-top: var(--s-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  background: var(--paper-alt);
  overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--t-small);
  line-height: 1.55;
}

.code-block:empty { display: none; }

.code-line { display: flex; white-space: pre; }
.code-line[data-error='true'] { background: #FEF6F2; }
.code-caret { color: var(--accent); }

.code-number {
  flex: none;
  width: 3.2rem;
  padding: 0 0.6rem;
  text-align: right;
  color: var(--ink-4);
  border-right: 1px solid var(--rule);
}

.code-body { padding: 0 0.7rem; }

/* ── Regex ─────────────────────────────────────────────────────────────── */

.regex-bar {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: var(--s-2);
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.regex-input { display: flex; align-items: center; gap: 0.35rem; }
.regex-input .input { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.regex-slash { color: var(--ink-4); font-size: var(--t-lead); }

.regex-flags { display: flex; gap: 0.3rem; flex-wrap: wrap; }

.flag {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--rule-strong);
  border-radius: 8px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-small);
  cursor: pointer;
}

.flag:has(input:checked) {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-tint);
}

.regex-highlight {
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-y: auto;
}

.regex-highlight mark {
  background: var(--accent-tint);
  color: var(--accent-deep);
  border-radius: 3px;
  padding: 0 1px;
}

.regex-groups:empty { display: none; }
.regex-groups { padding: 0 var(--s-3) var(--s-3); }

/* ── Diff ──────────────────────────────────────────────────────────────── */

.diff {
  border-bottom: 1px solid var(--rule);
  max-height: 30rem;
  overflow: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--t-small);
  line-height: 1.6;
}

.diff:empty { display: none; }

.diff-line { display: flex; white-space: pre-wrap; word-break: break-word; }
.diff-line[data-type='added'] { background: #EEF7F0; }
.diff-line[data-type='removed'] { background: #FDF1ED; }

.diff-number {
  flex: none;
  width: 3rem;
  padding: 0 0.5rem;
  text-align: right;
  color: var(--ink-4);
  user-select: none;
}

.diff-sign {
  flex: none;
  width: 1.4rem;
  text-align: center;
  user-select: none;
}

.diff-line[data-type='added'] .diff-sign { color: #1E7A46; }
.diff-line[data-type='removed'] .diff-sign { color: #9A3412; }
.diff-body { padding-right: var(--s-3); }

/* ── JWT panels ────────────────────────────────────────────────────────── */

.jwt-panels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1px;
  background: var(--rule);
  border-bottom: 1px solid var(--rule);
}

.jwt-panel { background: var(--paper); padding: var(--s-3); }
.jwt-panel .editor-area { padding: 0.5rem 0 0; min-height: 0; }

.jwt-signature-note {
  margin: 0.6rem 0 0;
  color: var(--ink-3);
  word-break: break-all;
}

.jwt-signature-note code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* An expired token is the single thing people are usually checking for. */
.meta-row[data-warn='true'] .meta-value { color: #9A3412; font-weight: 500; }

/* ── Tool notice ───────────────────────────────────────────────────────── */
/* A standing caveat a tool has to state before you use it, not after: the JWT
   decoder cannot verify a signature, and Image to Text is the one tool here
   that sends your file to a server. Quiet enough not to read as an error,
   placed above the input so it is read in time to matter. */

.tool-notice {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  margin: 0;
  padding: 0.85rem var(--s-3);
  font-size: var(--t-small);
  color: var(--ink-2);
  background: var(--paper-alt);
}

.tool-notice svg { flex: none; margin-top: 0.15rem; color: var(--ink-3); }

/* `display: flex` above beats the user agent's `[hidden]`, the same trap as
   `.menu-row`, `.install` and `.shot-strip`. PDF to Text hides this panel on
   every document that turned out not to need it, which is most of them. */
.tool-notice[hidden] { display: none; }

/* A second notice inside the result — PDF to Text offers the scanned pages
   there — sits directly under the one above the dropzone and shares its
   background, so without a rule between them the two read as one paragraph
   with two icons in it. */
.workbench-result .tool-notice { border-top: 1px solid var(--rule); }

/* ── Page selection ────────────────────────────────────────────────────── */

.split-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-3) 0;
}

.split-range {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3) 0;
}

.split-hint { color: var(--ink-3); }
.split-hint code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: var(--paper-alt);
  padding: 0.1rem 0.3rem;
  border-radius: 5px;
}

.split-error {
  min-height: 1.4em;
  margin: 0;
  padding: 0.35rem var(--s-3) 0;
  color: #9A3412;
}

/* "Every page separately" takes no range, so the field and the grid go. */
.workbench[data-split-mode='each'] [data-when-range],
.workbench[data-split-mode='each'] .page-grid,
.workbench[data-split-mode='each'] .split-error { display: none; }

.page-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  padding: var(--s-2) var(--s-3) var(--s-3);
  max-height: 16rem;
  overflow-y: auto;
}

.page-chip {
  min-width: 2.4rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--rule-strong);
  border-radius: 8px;
  background: var(--paper-raise);
  color: var(--ink-2);
  font: inherit;
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background 0.15s var(--ease), border-color 0.15s var(--ease);
}

.page-chip:hover { border-color: var(--accent); }

.page-chip[aria-pressed='true'] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
}


/* ── Orderable list ────────────────────────────────────────────────────── */

.order-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-bottom: 1px solid var(--rule);
  max-height: 28rem;
  overflow-y: auto;
}

.order-item {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--s-2);
  padding: 0.7rem var(--s-3);
  border-top: 1px solid var(--rule);
  background: var(--paper);
  cursor: grab;
}

.order-item:first-child { border-top: 0; }
.order-item[data-dragging='true'] { opacity: 0.4; cursor: grabbing; }

/* Where the row will land, shown on the edge it will land against. */
.order-item[data-drop='before'] { box-shadow: inset 0 2px 0 var(--accent); }
.order-item[data-drop='after'] { box-shadow: inset 0 -2px 0 var(--accent); }

.order-handle { color: var(--ink-4); letter-spacing: -0.15em; }

.order-name {
  font-size: var(--t-body-s);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.order-meta {
  font-size: var(--t-small);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A row that carries a preview, for Images to PDF. Ordering photographs by
   filename is guesswork — a camera calls them IMG_4471 and a scanner calls
   them scan_0003 — so the thumbnail is what makes the list usable at all. */
.order-item-thumb {
  grid-template-columns: auto auto auto minmax(0, 1fr) auto auto;
}

.order-thumb {
  display: block;
  width: 2.4rem;
  height: 2.4rem;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--rule);
  background: var(--paper-raise);
}

.order-controls { display: flex; gap: 0.2rem; }

.order-button {
  width: 1.9rem;
  height: 1.9rem;
  border: 1px solid var(--rule-strong);
  border-radius: 8px;
  background: var(--paper-raise);
  color: var(--ink-2);
  font: inherit;
  font-size: var(--t-body-s);
  line-height: 1;
  cursor: pointer;
}

.order-button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.order-button:disabled { opacity: 0.3; cursor: default; }

@media (max-width: 720px) {
  .order-item {
    grid-template-columns: auto minmax(0, 1fr) auto;
    row-gap: 0.2rem;
  }
  .order-handle { display: none; }
  .order-meta { grid-column: 2 / -1; }

  /* Both rows have to be placed explicitly, not left to auto-placement. The
     meta line is forced into column 3, which is already taken on row 1, so the
     controls would otherwise land on a third row in column 1 — and an `auto`
     column sized to a row of buttons drags the index number half way across
     the card. */
  .order-item-thumb { grid-template-columns: auto auto minmax(0, 1fr) auto; }
  .order-item-thumb .order-meta { grid-column: 3 / -1; grid-row: 2; }
  .order-item-thumb .order-controls { grid-column: 4; grid-row: 1; }
}

/* ── Metadata (EXIF) ───────────────────────────────────────────────────── */

.meta-layout {
  display: grid;
  grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
  gap: 1px;
  background: var(--rule);
  border-bottom: 1px solid var(--rule);
}

.meta-source,
.meta-panel { background: var(--paper); padding: var(--s-3); }

.meta-source img {
  width: 100%;
  max-height: 16rem;
  object-fit: contain;
  object-position: left top;
  border-radius: 10px;
  border: 1px solid var(--rule);
}

/* Image to Text can send a file the browser itself cannot decode — a HEIC
   outside Safari. There is no preview to show in that case, so the pane
   explains itself instead of holding a broken image icon. */
[data-preview='none'] .meta-source img,
.meta-source [data-preview-note] { display: none; }

[data-preview='none'] .meta-source [data-preview-note] { display: block; }

.meta-file {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin: 0.75rem 0 0;
  font-size: var(--t-body-s);
  word-break: break-all;
}

/* The location block only exists when there are coordinates to show — a
   permanently visible "no location" panel is noise on most photographs. */
.gps { display: none; margin-top: var(--s-3); }
.workbench[data-has-gps='true'] .gps { display: block; }

.gps-coords {
  margin: 0.3rem 0 0.6rem;
  font-variant-numeric: tabular-nums;
  font-size: var(--t-body-s);
}

.gps-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; }

.meta-summary {
  margin: 0 0 var(--s-2);
  font-size: var(--t-body-s);
  color: var(--ink-2);
}

.meta-table { display: flex; flex-direction: column; }

.meta-group {
  margin-top: var(--s-2);
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--ink-3);
}

.meta-group:first-child { margin-top: 0; }

.meta-row {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: var(--s-2);
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-body-s);
}

.meta-label { color: var(--ink-3); }
.meta-value { word-break: break-word; }

/* ── Table to CSV grid ─────────────────────────────────────────────────── */

.table-scroll {
  overflow-x: auto;
  border-top: 1px solid var(--rule);
}

.data-grid {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-body-s);
}

.data-grid th,
.data-grid td {
  border: 1px solid var(--rule);
  padding: 0;
  min-width: 5rem;
  vertical-align: top;
}

.data-grid th {
  background: var(--paper-alt);
  font-weight: 600;
}

.data-grid input {
  display: block;
  width: 100%;
  border: none;
  background: transparent;
  padding: 0.45rem 0.55rem;
  font: inherit;
  color: inherit;
}

.data-grid input:focus {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The clean-copy action only belongs on the remover. */
.workbench[data-mode='view'] [data-download-clean],
.workbench[data-mode='view'] [data-strip-summary] { display: none; }

@media (max-width: 900px) {
  .meta-layout { grid-template-columns: 1fr; }
  .meta-row { grid-template-columns: minmax(0, 9rem) minmax(0, 1fr); }
}

/* ── Colour palette ────────────────────────────────────────────────────── */

.palette-layout {
  display: grid;
  grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
  gap: 1px;
  background: var(--rule);
  border-bottom: 1px solid var(--rule);
}

.palette-source { background: var(--paper); padding: var(--s-3); }

.palette-source img {
  width: 100%;
  max-height: 20rem;
  object-fit: contain;
  object-position: left top;
  margin-top: 0.6rem;
  border-radius: 10px;
  border: 1px solid var(--rule);
}

.swatches {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--paper);
  display: flex;
  flex-direction: column;
}

.swatch {
  display: grid;
  grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
  align-items: stretch;
  border-bottom: 1px solid var(--rule);
}

.swatch:last-child { border-bottom: 0; }

.swatch-chip {
  display: flex;
  align-items: center;
  padding: 1.1rem var(--s-3);
  border: 0;
  background: var(--swatch);
  cursor: pointer;
  font: inherit;
}

/* The label sits on the colour, so its contrast is decided from the colour's
   own luminance rather than assumed. */
.swatch[data-on='dark'] .swatch-hex { color: #101314; }
.swatch[data-on='light'] .swatch-hex { color: #FFFFFF; }

.swatch-hex {
  font-size: var(--t-body-s);
  font-weight: 600;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}

.swatch-detail {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem var(--s-2);
  padding: 0.75rem var(--s-3);
}

.swatch-value {
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 0.2rem 0.45rem;
  background: transparent;
  font: inherit;
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
  cursor: pointer;
}

.swatch-value:hover { border-color: var(--rule-strong); color: var(--ink); }
[data-copied='true'] { outline: 2px solid var(--accent); outline-offset: 1px; }

@media (max-width: 900px) {
  .palette-layout { grid-template-columns: 1fr; }
  .swatch { grid-template-columns: 8rem minmax(0, 1fr); }
}

/* ── QR generator ──────────────────────────────────────────────────────── */

.qr-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 22rem);
  gap: 1px;
  background: var(--rule);
  border-bottom: 1px solid var(--rule);
}

.qr-form,
.qr-preview { background: var(--paper); padding: var(--s-3); }

.qr-form { display: flex; flex-direction: column; gap: var(--s-2); }
.segmented-wrap { flex-wrap: wrap; }

/* Only the field group for the selected type is shown; the switch sets the
   attribute, so the script never shows or hides anything itself. */
.qr-fields { display: none; flex-direction: column; gap: 0.75rem; }
.workbench[data-qr-kind='url'] [data-fields='url'],
.workbench[data-qr-kind='text'] [data-fields='text'],
.workbench[data-qr-kind='email'] [data-fields='email'],
.workbench[data-qr-kind='phone'] [data-fields='phone'],
.workbench[data-qr-kind='sms'] [data-fields='sms'],
.workbench[data-qr-kind='wifi'] [data-fields='wifi'] { display: flex; }

.field-block { align-items: stretch; }
.qr-optional { color: var(--ink-4); text-transform: none; letter-spacing: 0; }

.qr-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
}

.qr-stage {
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: 1;
  max-width: 18rem;
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  background: var(--paper-raise);
  padding: var(--s-2);
}

/* The <img> is only meaningful once a code exists; until then the note
   stands in its place. */
.qr-stage img { display: none; width: 100%; height: auto; image-rendering: pixelated; }
.workbench[data-state='result'] .qr-stage img { display: block; }
.workbench[data-state='result'] .qr-empty { display: none; }
.qr-empty { max-width: 22ch; text-align: center; }

/* Two lines are reserved whether or not there is anything to say. The caption
   goes from "—" to "29 × 29 modules · 481 × 481 px · 38 characters" the moment
   you type a character, and letting it grow shunted the whole panel down —
   jarring on a tool that updates on every keystroke. */
.qr-meta {
  color: var(--ink-3);
  text-align: center;
  min-height: 2.6em;
  margin: 0;
}

@media (max-width: 900px) {
  .qr-layout { grid-template-columns: 1fr; }
  .qr-preview { order: -1; }
}

/* ── Stat grid ─────────────────────────────────────────────────────────── */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 1px;
  background: var(--rule);
  border-bottom: 1px solid var(--rule);
}

.stat-cell {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: var(--s-3) var(--s-2);
  background: var(--paper);
}

.stat-big {
  font-size: clamp(1.35rem, 2.4vw, 1.85rem);
  font-weight: 600;
  letter-spacing: -0.025em;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

@media (max-width: 900px) {
  .editor { grid-template-columns: 1fr; }
  .stat-grid { grid-template-columns: repeat(3, 1fr); }
}

@media (max-width: 520px) {
  .stat-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ── Tool controls ───────────────────────────────────────────────────────
   Inputs, segmented switches and preset chips, shared by every tool that
   needs settings rather than just a file. */

.field-row {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-3) 0;
}

.field-group { display: flex; align-items: flex-end; gap: 0.6rem; }

/* Only the group matching the active mode is shown; the switch sets the
   attribute, so there is no show/hide logic in the tool script. */
.workbench[data-resize-mode='dimensions'] [data-when='percent'],
.workbench[data-resize-mode='percent'] [data-when='dimensions'] { display: none; }
.workbench[data-json-style='minify'] [data-when='pretty'] { display: none; }

/* The background picker only matters for a target with no alpha channel, and
   the quality slider only for a lossy one. Both are attribute-driven so the
   tool script never shows or hides anything itself. */
.workbench[data-opaque='false'] [data-when='opaque'],
.workbench[data-lossy='false'] [data-when='lossy'] { display: none; }

/* "Fit to image" makes the page the shape of the picture, so there is no
   orientation left to choose. Quality is a JPG setting; PNG ignores it. */
.workbench[data-page-mode='fit'] [data-when='paged'],
.workbench[data-image-format='png'] [data-when='jpeg'] { display: none; }

.field { display: flex; flex-direction: column; gap: 0.3rem; }
.field-wide { flex: 1; min-width: 12rem; }
.field-x { color: var(--ink-4); padding-bottom: 0.7rem; }

/* Fills the field it sits in. A fixed width here was wrong: it made every
   text input 112px regardless of the space available, and it silently beat
   `.input-block { width: 100% }` on source order, so opting out did nothing.
   Fields that genuinely want to be small opt in with .input-narrow. */
.input {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-control);
  background: var(--paper-raise);
  font: inherit;
  font-size: var(--t-body-s);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.input:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

/* --- .input modifiers ---------------------------------------------------
   Every one of these overrides a property .input sets, so they have to come
   after it. Two of them used to live in other sections and lost the width
   they declared; see the ordering note at the top of this file. */

/* Numeric fields that hold three or four digits — a full-width box for "1200"
   reads as though something longer is expected. */
.input-narrow { width: 7rem; }

/* A page-range field holds "1-3, 7, 12-" and no more. */
.input-range { width: 12rem; }

/* A select sizes to its longest option; stretching it to the row adds nothing. */
.input-select { width: auto; padding-right: 1.6rem; }

/* A colour well is a swatch, not a text field. */
.input-color {
  width: 3rem;
  height: 2.1rem;
  padding: 2px;
  cursor: pointer;
}

textarea.input { min-height: 4.5rem; resize: vertical; }

.segmented {
  display: inline-flex;
  padding: 3px;
  gap: 3px;
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-control);
  background: var(--paper-alt);
}

.segment {
  padding: 0.42rem 0.8rem;
  border: 0;
  border-radius: calc(var(--r-control) - 4px);
  background: transparent;
  font: inherit;
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink-2);
  cursor: pointer;
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}

.segment[aria-pressed='true'] {
  background: var(--paper-raise);
  color: var(--ink);
  box-shadow: var(--lift);
}

.btn-toggle[aria-pressed='false'] { opacity: 0.5; }
.btn-toggle[aria-pressed='true'] { border-color: var(--accent); color: var(--accent); }

.presets {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: var(--s-2) var(--s-3) 0;
}

@media (max-width: 720px) {
  .field-row { align-items: stretch; flex-direction: column; }
  .field-group { flex-wrap: wrap; }
  /* The workbench inputs that need to be narrow on mobile already opt in with
     .input-narrow / .input-range. A global `.input { width: 5.5rem }` here
     collapsed the email and password fields on the auth pages to 88px — they
     are full-width of the form column, the same width as the social buttons
     above them. */
}

.compare {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--rule);
  border-bottom: 1px solid var(--rule);
}

.compare-pane {
  background: var(--paper);
  padding: var(--s-3);
}

.compare-pane img {
  width: 100%;
  max-height: 320px;
  object-fit: contain;
  object-position: left top;
  border-radius: 10px;
  border: 1px solid var(--rule);
  background:
    conic-gradient(from 90deg, #F1F1EC 0 25%, #FAFAF8 0 50%, #F1F1EC 0 75%, #FAFAF8 0) 0 0 / 16px 16px;
}

.stat-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-top: 0.75rem;
}

.stat-value {
  font-size: 1.375rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.stat-delta { color: var(--accent); font-weight: 600; font-size: var(--t-body-s); }

.control-row {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-3);
  flex-wrap: wrap;
}

/* ── Compliance report (Make Image Fit / Make PDF Fit) ─────────────────── */
/* The report is the product on these pages, so it sits beside the result
   rather than under it, and it is the thing that stays legible when the
   preview is a passport photo three centimetres tall.

   Three row states, and the third is the reason the component exists: `note`
   is a requirement the rule makes and software cannot check. It must not read
   as a pass, so it takes ink-3 and a hairline mark rather than the accent. */

.report {
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0;
}

.report-row {
  display: grid;
  grid-template-columns: 1.1rem minmax(0, 1fr);
  gap: 0 var(--s-2);
  align-items: start;
  padding: 0.55rem 0;
  border-top: 1px solid var(--rule);
  font-size: var(--t-body-s);
}

.report-row:first-child { border-top: none; }

.report-mark {
  width: 1.1rem;
  height: 1.1rem;
  margin-top: 0.05rem;
}

.report-row[data-state='pass'] .report-mark { color: var(--accent); }
.report-row[data-state='fail'] .report-mark { color: var(--flag); }
.report-row[data-state='note'] .report-mark { color: var(--ink-4); }

.report-row[data-state='fail'] .report-label { color: var(--flag); font-weight: 600; }
.report-row[data-state='note'] .report-label { color: var(--ink-3); }

.report-detail {
  grid-column: 2;
  color: var(--ink-3);
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;
}

/* The citation. Quiet, but never hidden — a rule with no attributable source
   is the thing these pages exist not to be. */
.report-source {
  margin: 0;
  padding: 0.6rem var(--s-3);
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-small);
  color: var(--ink-3);
}

.report-source a { color: var(--ink-2); }

.fit-result {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  border-bottom: 1px solid var(--rule);
}

.fit-preview {
  padding: var(--s-3);
  border-right: 1px solid var(--rule);
  min-width: 0;
}

.fit-preview img {
  max-width: 100%;
  height: auto;
  border: 1px solid var(--rule);
  border-radius: 8px;
}

.fit-report { padding: var(--s-3); min-width: 0; }

.fit-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

/* On a target page the rule is fixed and there is nothing to configure. */
.workbench[data-mode='target'] .fit-controls { display: none; }

@media (max-width: 46rem) {
  .fit-result { grid-template-columns: minmax(0, 1fr); }
  .fit-preview {
    border-right: none;
    border-bottom: 1px solid var(--rule);
  }
}

/* ── Shrink readout (PDF Compressor) ───────────────────────────────────── */
/* The whole result is one number, so it is set at display size rather than
   buried in a stats grid. The bar under it is the share of the original that
   remains — the empty space to its right is the saving, which reads faster
   than two bars the eye has to compare. */

.shrink-readout {
  padding: var(--s-4) var(--s-3) var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.shrink-figure {
  margin: 0;
  font-size: clamp(1.6rem, 1.1rem + 2vw, 2.4rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}

.shrink-detail { margin: 0.35rem 0 0; }

.shrink-bar {
  margin-top: var(--s-3);
  height: 6px;
  border-radius: 3px;
  background: var(--rule);
  overflow: hidden;
}

.shrink-bar span {
  display: block;
  height: 100%;
  /* Set by the script; full until a result says otherwise, so the bar is
     never a sliver of nothing while the first file is still being read. */
  width: var(--fill, 100%);
  border-radius: 3px;
  background: var(--accent);
  transition: width 420ms var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  .shrink-bar span { transition: none; }
}

.slider {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  min-width: 180px;
  height: 2px;
  background: var(--rule-ink);
  border-radius: 2px;
  outline: none;
}

.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--paper-raise);
  border: 1px solid var(--rule-ink);
  box-shadow: var(--lift);
  cursor: pointer;
  transition: border-color 0.18s var(--ease);
}

.slider::-webkit-slider-thumb:hover { border-color: var(--accent); }

.slider::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--paper-raise);
  border: 1px solid var(--rule-ink);
  cursor: pointer;
}

/* Content beneath the tool */
.article {
  max-width: 68ch;
}

.article h2 {
  font-size: 1.625rem;
  font-weight: 600;
  letter-spacing: -0.022em;
  margin-bottom: var(--s-2);
}

.article h3 {
  font-size: var(--t-body);
  font-weight: 600;
  letter-spacing: -0.012em;
  margin-bottom: 0.35rem;
}

.article p {
  color: var(--ink-2);
  margin-bottom: var(--s-2);
  line-height: 1.68;
}

.article p:last-child { margin-bottom: 0; }

.steps {
  counter-reset: step;
  border-top: 1px solid var(--rule);
  margin-top: var(--s-3);
}

.steps li {
  counter-increment: step;
  display: grid;
  grid-template-columns: 3rem minmax(0, 1fr);
  gap: var(--s-2);
  padding-block: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.steps li::before {
  content: '0' counter(step);
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  padding-top: 0.35rem;
}

.steps p { font-size: var(--t-body-s); color: var(--ink-3); margin: 0.2rem 0 0; }

/* ── Privacy strip ───────────────────────────────────────────────────────
   Sits directly under the tool, because "does this upload my file?" is the
   question people ask before they use it, not after they read the FAQ. */

/* ── Screenshot stage ──────────────────────────────────────────────────── */
/* The result already carries its own background, so the stage behind it is a
   neutral surface rather than another colour competing with the one the user
   just chose. The chequerboard shows through when they pick Transparent —
   otherwise nothing would distinguish it from white. */

.shot-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-4) var(--s-3);
  border-bottom: 1px solid var(--rule);
  background-color: var(--paper-alt);
  background-image:
    linear-gradient(45deg, var(--rule) 25%, transparent 25%),
    linear-gradient(-45deg, var(--rule) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--rule) 75%),
    linear-gradient(-45deg, transparent 75%, var(--rule) 75%);
  background-size: 16px 16px;
  background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}

.shot-stage img {
  max-width: 100%;
  max-height: 26rem;
  object-fit: contain;
  border-radius: 4px;
}

.shot-controls { padding: var(--s-3); display: grid; gap: var(--s-2); }

.shot-row {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
}

.shot-label { flex: none; width: 6rem; color: var(--ink-2); }

.shot-value {
  flex: none;
  width: 3.5rem;
  text-align: right;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* Where a setting has a position but no unit. The Signature Extractor's
   sensitivity is a threshold on relative darkness: a number beside it would be
   precise and unreadable, so the row says which way to drag instead. Not
   .shot-value — that slot is a fixed 3.5rem of tabular figures. */
.shot-hint { color: var(--ink-3); }

/* --- Handwriting to Font -------------------------------------------------- */

/* The one tool whose empty state is two steps rather than one dropzone: there
   is nothing to drop until the sheet has been printed and written on. Numbered
   rather than stacked, because the order is the instruction. */
.steps-inline {
  display: grid;
  gap: var(--s-4);
  max-width: 46rem;
  margin: 0 auto;
  text-align: left;
}

.step-inline {
  display: flex;
  gap: var(--s-2);
  align-items: flex-start;
}

.step-inline h2 { font-size: var(--t-h3); margin-bottom: 0.2rem; }
.step-inline .prose-note { margin-bottom: 0.9rem; max-width: 42ch; }

.step-number {
  flex: none;
  width: 1.6rem;
  height: 1.6rem;
  display: grid;
  place-items: center;
  border: 1px solid var(--rule-strong);
  border-radius: 50%;
  color: var(--ink-2);
}

/* Like .shot-stage, without the chequerboard. The specimen card is opaque, and
   a transparency pattern behind it says something about the file that is not
   true. */
.card-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-4) var(--s-3);
  border-bottom: 1px solid var(--rule);
  background: var(--paper-alt);
}

.card-stage img {
  max-width: 100%;
  max-height: 26rem;
  object-fit: contain;
  box-shadow: var(--lift);
}

/* A line of the visitor's own font, at reading size. `cursive` is the fallback
   deliberately: if the face failed to load, a line set in the browser's script
   font is obviously not your handwriting, where a sans-serif fallback would
   look like a tool that had merely done nothing. */
.specimen {
  margin-top: var(--s-2);
  padding: var(--s-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  background: var(--paper-raise);
  font-size: 1.75rem;
  line-height: 1.5;
  color: var(--ink);
  overflow-wrap: anywhere;
}

@media (min-width: 40rem) {
  .steps-inline { grid-template-columns: 1fr 1fr; gap: var(--s-3); }
}

/* Named for this tool: .swatch and .swatches already belong to the Colour
   Palette Extractor, and borrowing them meant its grid rules fought these
   flex ones — the six circles stacked into a column. */
.shot-swatches { display: flex; gap: 0.5rem; flex-wrap: wrap; }

.shot-swatch {
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--rule-ink);
  border-radius: 50%;
  cursor: pointer;
  transition: transform 120ms var(--ease), box-shadow 120ms var(--ease);
}

.shot-swatch:hover { transform: scale(1.08); }

.shot-swatch[aria-pressed='true'] {
  box-shadow: 0 0 0 2px var(--paper), 0 0 0 3px var(--ink);
}

.shot-swatch-slate  { background: linear-gradient(135deg, #1E293B, #0F172A); }
.shot-swatch-dawn   { background: linear-gradient(135deg, #FDA4AF, #FB923C); }
.shot-swatch-mint   { background: linear-gradient(135deg, #6EE7B7, #3B82F6); }
.shot-swatch-violet { background: linear-gradient(135deg, #A78BFA, #6366F1); }
.shot-swatch-paper  { background: linear-gradient(135deg, #FAFAF8, #E7E5E0); }

/* Transparent needs to look like transparency, not like white. */
.shot-swatch-none {
  background-color: #FFF;
  background-image:
    linear-gradient(45deg, #C9C7C1 25%, transparent 25%),
    linear-gradient(-45deg, #C9C7C1 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, #C9C7C1 75%),
    linear-gradient(-45deg, transparent 75%, #C9C7C1 75%);
  background-size: 10px 10px;
  background-position: 0 0, 0 5px, 5px -5px, -5px 0;
}

@media (max-width: 640px) {
  .shot-label { width: 100%; }
  .shot-value { margin-left: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .shot-swatch { transition: none; }
  .shot-swatch:hover { transform: none; }
}

/* ── Crop ──────────────────────────────────────────────────────────────── */
/* The overlay is the preview: everything outside the box is dimmed rather than
   hidden, so the crop is always read against the picture it came out of. Like
   the redaction overlay below, the box is positioned in percentages of the
   frame — the crop is held in image pixels and only ever converted at the edge,
   which is what lets the same selection survive the picture being scaled down
   to fit a phone. */

.crop-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-4) var(--s-3);
  background: var(--paper-alt);
  border-bottom: 1px solid var(--rule);
}

/* Shrink-wrapped to the picture, because the overlay's percentages are
   measured against this box. Any padding or line-height here would put the
   crop out of register with what it is drawn over. */
.crop-frame {
  position: relative;
  display: inline-block;
  max-width: 100%;
  line-height: 0;
  overflow: hidden;
  cursor: crosshair;
  touch-action: none;
  box-shadow: 0 1px 3px rgba(16, 19, 20, 0.10), 0 8px 24px rgba(16, 19, 20, 0.06);
}

.crop-frame img {
  display: block;
  max-width: 100%;
  max-height: 28rem;
  user-select: none;
  -webkit-user-drag: none;
}

/* The dimming is one huge spread shadow rather than four positioned panels:
   it can never leave a seam, and it costs nothing to move. `overflow: hidden`
   on the frame is what keeps it off the rest of the page. */
.crop-box {
  position: absolute;
  cursor: move;
  outline: 1px solid var(--paper);
  box-shadow: 0 0 0 9999px rgba(16, 19, 20, 0.55);
}

.crop-box:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; }

/* Thirds, the composition guide every camera draws. Faint enough to read the
   picture through. */
.crop-box::before,
.crop-box::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-repeat: no-repeat;
}

.crop-box::before {
  background-image:
    linear-gradient(to right, rgba(250, 250, 248, 0.35) 1px, transparent 1px),
    linear-gradient(to right, rgba(250, 250, 248, 0.35) 1px, transparent 1px);
  background-size: 1px 100%;
  background-position: 33.333% 0, 66.666% 0;
}

.crop-box::after {
  background-image:
    linear-gradient(to bottom, rgba(250, 250, 248, 0.35) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(250, 250, 248, 0.35) 1px, transparent 1px);
  background-size: 100% 1px;
  background-position: 0 33.333%, 0 66.666%;
}

.crop-handle {
  position: absolute;
  width: 13px;
  height: 13px;
  background: var(--paper);
  border: 1px solid var(--ink);
  border-radius: 2px;
  /* Centred on the corner they sit at, so the grab point is the corner rather
     than a square hanging off the inside of it. */
  transform: translate(-50%, -50%);
}

.crop-handle-nw { left: 0;    top: 0;    cursor: nwse-resize; }
.crop-handle-n  { left: 50%;  top: 0;    cursor: ns-resize; }
.crop-handle-ne { left: 100%; top: 0;    cursor: nesw-resize; }
.crop-handle-e  { left: 100%; top: 50%;  cursor: ew-resize; }
.crop-handle-se { left: 100%; top: 100%; cursor: nwse-resize; }
.crop-handle-s  { left: 50%;  top: 100%; cursor: ns-resize; }
.crop-handle-sw { left: 0;    top: 100%; cursor: nesw-resize; }
.crop-handle-w  { left: 0;    top: 50%;  cursor: ew-resize; }

.crop-controls {
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

/* `crop-controls` already pays for the padding around this row, and `presets`
   brings its own — the two together indented the whole line twice. */
.crop-shapes { padding: 0; }

/* Flows with the chips rather than being pushed to the far right. There are
   seven shapes, so this wraps to a second line at most widths, and `auto`
   margin left it stranded on its own out at the right edge — a button with no
   neighbours reads as though it belongs to something else. */
.crop-reset { flex: none; margin-left: 0.35rem; }

.crop-source { color: var(--ink-3); margin: var(--s-2) 0 0; }

/* A `kbd` is taller than the text around it, so the line box it sits in grows
   and the paragraph's own line-height no longer describes it. Without the
   floor here the hint's first line rode up into the file name above it. */
.crop-hint {
  margin: 0.3rem 0 0;
  max-width: 62ch;
  line-height: 1.9;
}

.crop-hint kbd { min-width: 0; padding: 0.1rem 0.32rem; }

/* Fingers are not pointers: the handles have to be big enough to hit without
   swallowing a small crop box whole. */
@media (pointer: coarse) {
  .crop-handle { width: 20px; height: 20px; }
}

/* ── Rotate PDF ────────────────────────────────────────────────────────── */
/* Thumbnails rather than a page grid of numbers, which Split PDF gets away
   with: "which pages are sideways" is a question only the pictures answer. */

.rot-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  flex-wrap: wrap;
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.rot-buttons { display: flex; gap: 0.35rem; }

.rot-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--s-3);
  padding: var(--s-3);
  max-height: 42rem;
  overflow-y: auto;
  background: var(--paper-alt);
  border-bottom: 1px solid var(--rule);
}

.rot-page {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 0;
}

/* A fixed height so a page turning between portrait and landscape does not
   drag every thumbnail after it up and down the grid. */
.rot-sheet {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 150px;
  padding: 0.4rem;
  background: var(--paper);
  border: 1px solid var(--rule);
}

.rot-canvas {
  display: block;
  max-width: 100%;
  max-height: 100%;
  box-shadow: 0 1px 2px rgba(16, 19, 20, 0.12);
}

.rot-page[data-turned='true'] .rot-sheet { border-color: var(--accent); }

.rot-bar {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.rot-number { color: var(--ink-2); flex: none; }

.rot-angle {
  color: var(--ink-3);
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rot-page[data-turned='true'] .rot-angle { color: var(--accent); }

.rot-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  color: var(--ink-2);
  background: var(--paper);
  border: 1px solid var(--rule-ink);
  border-radius: var(--r-control);
  cursor: pointer;
}

.rot-button:hover { border-color: var(--accent); color: var(--accent); }

/* A phone is fifteen pixels short of fitting two 140px tracks beside a 24px
   gap, so it falls all the way back to one — and a portrait page in a
   full-width cell is a 95px thumbnail adrift in a 289px box. Trimming both
   numbers buys the second column back. */
@media (max-width: 640px) {
  .rot-grid {
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
    gap: var(--s-2);
    padding: var(--s-2);
  }
}

/* ── PDF redaction ─────────────────────────────────────────────────────── */
/* Pages are rendered at a fixed scale and then allowed to shrink to fit, so
   the redaction overlay is positioned in percentages rather than pixels — see
   `place()` in the tool. A box pinned to pixels drifts off its text the moment
   the sheet is narrower than it rendered. */

.page-stage {
  display: grid;
  gap: var(--s-3);
  justify-items: center;
  padding: var(--s-3);
  max-height: 42rem;
  overflow-y: auto;
  background: var(--paper-alt);
  border-bottom: 1px solid var(--rule);
}

.page-sheet {
  position: relative;
  max-width: 100%;
  line-height: 0;
  box-shadow: 0 1px 3px rgba(16, 19, 20, 0.10), 0 8px 24px rgba(16, 19, 20, 0.06);
}

.page-canvas {
  display: block;
  max-width: 100%;
  height: auto;
  /* The whole surface is a drawing target, so say so. */
  cursor: crosshair;
  touch-action: none;
}

.redaction {
  position: absolute;
  margin: 0;
  padding: 0;
  border: 0;
  background: #000;
  cursor: pointer;
}

/* Solid black in the output; while editing it shows a hairline so a box drawn
   over an already-black area is still visible as *your* box. */
.redaction:not(.redaction-ghost) { outline: 1px solid var(--accent); }
.redaction:hover { opacity: 0.72; }

.redaction-ghost { opacity: 0.45; pointer-events: none; }

.audit {
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
}

.audit-head { cursor: pointer; }
.audit-note { margin: 0.5rem 0 var(--s-2); max-width: 62ch; }

.audit-row {
  border-top: 1px solid var(--rule);
  padding: 0.6rem 0;
}

.audit-row summary { cursor: pointer; display: flex; gap: 0.6rem; }

.audit-text {
  margin: 0.5rem 0 0;
  font-size: var(--t-body-s);
  color: var(--ink-2);
  max-height: 12rem;
  overflow-y: auto;
  word-break: break-word;
}

/* ── Keyboard shortcuts ────────────────────────────────────────────────── */
/* Closed by default and quiet when closed: this is a hint for the people who
   look for one, not a feature the page needs to argue for. */

.shortcuts { margin-top: var(--s-2); }

.shortcuts summary {
  cursor: pointer;
  color: var(--ink-3);
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

.shortcuts summary::-webkit-details-marker { display: none; }
.shortcuts summary::before { content: '⌘'; font-size: 0.85em; }
.shortcuts summary:hover { color: var(--ink-2); }

.shortcut-list {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.55rem var(--s-2);
  align-items: baseline;
  margin: var(--s-2) 0 0;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  background: var(--paper-alt);
  max-width: 34rem;
}

.shortcut-list dt { display: flex; align-items: center; gap: 0.25rem; }
.shortcut-list dd { margin: 0; font-size: var(--t-body-s); color: var(--ink-2); }
.shortcut-list .or { color: var(--ink-3); font-size: var(--t-micro); margin: 0 0.15rem; }

kbd {
  font-family: inherit;
  font-size: var(--t-micro);
  font-weight: 500;
  min-width: 1.55rem;
  padding: 0.2rem 0.4rem;
  text-align: center;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule-ink);
  border-bottom-width: 2px;
  border-radius: 5px;
}

/* ── Favourite ─────────────────────────────────────────────────────────── */
/* Top right of the tool header, out of the reading path. The star is a fact
   about this page for the person signed in, not a call to action — so it is
   quiet until hovered and only fills once it means something. */

/* Floated, so the heading's text wraps around it rather than being pushed
   down — but a float only displaces the *line boxes* of what follows, not its
   box. The h1 therefore overlapped it, and because `.reveal` animates a
   transform the h1 got its own stacking context and painted on top. The button
   was visible, correctly wired, and swallowed every click.

   `position: relative` puts it back in front. Worth the two lines: the symptom
   is a button that looks fine and does nothing, which is indistinguishable
   from a broken handler and sends you looking in the wrong place. */
.favorite-wrap {
  float: right;
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-left: var(--s-2);
}

.favorite {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.4rem 0.75rem;
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 999px;
  cursor: pointer;
  transition: color 140ms var(--ease), border-color 140ms var(--ease);
}

.favorite:hover { color: var(--ink); border-color: var(--rule-strong); }
.favorite:disabled { opacity: 0.55; cursor: default; }

/* Filled only when it is true. */
.favorite[aria-pressed='true'] { color: var(--accent); border-color: var(--accent); }
.favorite[aria-pressed='true'] svg { fill: var(--accent); }

.favorite-note { color: var(--ink-3); max-width: 22ch; }

@media (max-width: 640px) {
  .favorite-wrap { float: none; margin: 0 0 var(--s-2); }
}

@media (prefers-reduced-motion: reduce) { .favorite { transition: none; } }

/* ── Saved tools ───────────────────────────────────────────────────────── */
/* Used on the account page and in the homepage shelf. Cards rather than a
   list: a favourite is something you are navigating to, and a target the size
   of a word is a worse one than a target the size of a card. */

.favorite-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 1px;
  background: var(--rule);
  border-block: 1px solid var(--rule);
}

.favorite-card {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  padding: var(--s-3);
  background: var(--paper);
  text-decoration: none;
  color: inherit;
  transition: background 0.22s var(--ease);
}

.favorite-card:hover { background: var(--paper-raise); }

.favorite-card-icon { flex: none; color: var(--cat, var(--accent)); line-height: 0; }
.favorite-card-icon svg { width: 20px; height: 20px; }

.favorite-card-name { font-size: 1rem; font-weight: 500; letter-spacing: -0.012em; }

.favorite-card-note {
  font-size: var(--t-body-s);
  color: var(--ink-3);
  margin-top: 0.15rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.favorite-chips[hidden], [data-examples][hidden] { display: none; }
.favorite-chips [data-favorite-chip-list] { display: contents; }
.chip-quiet { color: var(--ink-3); }

/* A chip that goes somewhere, as opposed to one that fills the search box. */
.chip-tool { display: inline-flex; align-items: center; gap: 0.45rem; }
.chip-tool .cat-dot { background: var(--cat, var(--accent)); }
.auth-empty { margin: var(--s-2) 0 0; }

.nav-fav {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--accent);
}

.nav-fav[hidden] { display: none; }
.nav-fav svg { fill: currentColor; }

/* ── Turnstile ─────────────────────────────────────────────────────────── */
/* The container is in the markup on every AI tool page but is empty and zero
   height for a normal visitor — Managed mode only draws a checkbox for traffic
   Cloudflare distrusts. So the spacing hangs off `:has(iframe)` rather than the
   container itself: a margin here unconditionally would open a gap under the
   workbench on every page, for the large majority who are never challenged.
   Without it the checkbox sits flush against the workbench border. */

#turnstile-container:has(iframe) { margin: var(--s-2) 0; }

.privacy-strip {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: var(--s-2) 0 0;
  padding: 0.85rem var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  background: var(--paper-alt);
  font-size: var(--t-small);
  color: var(--ink-2);
}

.privacy-strip svg {
  width: 15px;
  height: 15px;
  flex: none;
  color: var(--accent);
}

/* ── Model attribution ───────────────────────────────────────────────────
   The one element on the site carrying a vendor's own colour, on purpose.
   White rather than --paper-alt so it separates from the privacy strip
   stacked directly beneath it, and so the mark sits on its intended ground.
   Rendered from tool.html under `tool.ai`; see website/_gemini_credit.html.
   ────────────────────────────────────────────────────────────────────── */

.gemini-credit {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin: var(--s-3) 0 0;
  padding: 0.9rem var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  background: var(--paper-raise);
}

.gemini-credit svg {
  width: 26px;
  height: 26px;
  flex: none;
  margin-top: 0.05rem;
}

.gemini-credit p {
  margin: 0;
  font-size: var(--t-small);
  line-height: 1.55;
  color: var(--ink-2);
}

.gemini-credit strong {
  display: block;
  color: var(--ink);
  font-weight: 600;
}

/* ── Use cases ───────────────────────────────────────────────────────── */

.use-cases {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1px;
  margin-top: var(--s-3);
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  overflow: hidden;
}

.use-case { background: var(--paper); padding: var(--s-3); }

.use-case h3 {
  font-size: var(--t-body-s);
  font-weight: 600;
  letter-spacing: -0.012em;
  margin: 0 0 0.35rem;
}

.use-case p {
  font-size: var(--t-body-s);
  color: var(--ink-3);
  margin: 0;
}

/* ── Advertising ─────────────────────────────────────────────────────────
   Rules, not suggestions:
     · never above or inside the tool — this slot lives after the FAQ;
     · never dressed as product UI. An ad slot may not use --paper-raise, and
       may not carry .btn, .workbench, .tool-card or .dropzone. A "download"
       button that is actually an advert is the single most common dark
       pattern on utility sites and the thing Pathine is positioned against;
     · min-height is reserved so filling the slot later costs no layout shift.
   The slot is empty until there is an ad network behind it; an empty
   reserved box is deliberate, not an oversight. */

.ad-slot {
  display: block;
  min-height: 90px;
  margin: var(--s-4) 0 0;
  border: 1px dashed var(--rule-strong);
  border-radius: var(--r-control);
  background: transparent;
}

.ad-slot:empty { border-color: var(--rule); }

@media (min-width: 720px) {
  .ad-slot { min-height: 120px; }
}

.faq { border-top: 1px solid var(--rule); }

.faq details {
  border-bottom: 1px solid var(--rule);
}

.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  padding-block: var(--s-3);
  cursor: pointer;
  list-style: none;
  font-weight: 500;
  letter-spacing: -0.012em;
  font-size: var(--t-body-s);
}

.faq summary::-webkit-details-marker { display: none; }

.faq summary::after {
  content: '';
  width: 11px;
  height: 11px;
  flex: none;
  border-right: 1px solid var(--ink-3);
  border-bottom: 1px solid var(--ink-3);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 0.24s var(--ease);
}

.faq details[open] summary::after {
  transform: rotate(225deg) translate(-2px, -2px);
}

.faq details p {
  font-size: var(--t-body-s);
  color: var(--ink-3);
  padding-bottom: var(--s-3);
  max-width: 62ch;
}

/* Related tools — a ruled list, not a card farm */
.related {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--rule);
  border-block: 1px solid var(--rule);
}

.related-item {
  background: var(--paper);
  padding: var(--s-3);
  display: flex;
  align-items: center;
  gap: 0.75rem;
  transition: background 0.2s var(--ease);
}

.related-item:hover { background: var(--paper-raise); }
.related-item span { font-size: var(--t-body-s); font-weight: 500; letter-spacing: -0.012em; }
.related-item svg {
  width: 15px;
  height: 15px;
  margin-left: auto;
  color: var(--ink-4);
  opacity: 0;
  transition: opacity 0.2s var(--ease);
}
.related-item:hover svg { opacity: 1; color: var(--accent); }

/* --- Category page -------------------------------------------------------- */

.subgroup { margin-bottom: var(--s-5); }

.subgroup-head {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--rule-strong);
  margin-bottom: 0;
}

.subgroup-head h2 {
  font-size: var(--t-h3);
  font-weight: 500;
  letter-spacing: -0.018em;
}

.listing { border-bottom: 1px solid var(--rule); }

.listing-row {
  display: grid;
  grid-template-columns: 3rem minmax(0, 1.1fr) minmax(0, 1.6fr) auto;
  align-items: center;
  gap: var(--s-2);
  padding-block: var(--s-3);
  border-bottom: 1px solid var(--rule);
  position: relative;
  transition: padding-left 0.24s var(--ease);
}

.listing-row:last-child { border-bottom: 0; }

.listing-row::before {
  content: '';
  position: absolute;
  left: 0;
  top: -1px;
  bottom: -1px;
  width: 2px;
  background: var(--cat, var(--accent));
  transform: scaleY(0);
  transition: transform 0.28s var(--ease);
}

.listing-row:hover::before { transform: scaleY(1); }
.listing-row:hover { padding-left: 0.75rem; }

.listing-name {
  font-size: var(--t-body);
  font-weight: 500;
  letter-spacing: -0.016em;
}

.listing-desc { font-size: var(--t-body-s); color: var(--ink-3); }

.listing-go {
  width: 16px;
  height: 16px;
  color: var(--ink-4);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity 0.22s var(--ease), transform 0.22s var(--ease);
}

.listing-row:hover .listing-go { opacity: 1; transform: translateX(0); color: var(--accent); }

/* --- Account ---------------------------------------------------------------
   Sign in, sign up, password reset and the account page.

   Everything below reuses the system rather than inventing an auth look:
   `.field` and `.input` are the same controls the tools use, `.btn` is the same
   button, the index motif at the top of each page is the one from the hero.
   That is the point. The pages these replace came from another product
   entirely — dark, lime green, Space Grotesk off a CDN, and a headline about
   tracking your week — so signing in meant visibly leaving the site.

   Declared here, near the end, because several of these are modifiers of bases
   defined much earlier (`.btn` at ~1326) and the ordering rule at the top of
   this file applies. See tests/test_stylesheet.py.
   -------------------------------------------------------------------------- */

.auth {
  display: flex;
  justify-content: center;
  padding-block: clamp(2.5rem, 7vw, 5rem) var(--s-7);
}

/* 26rem holds an email address at 15px without wrapping and keeps the form
   narrower than a line of body text, which is what makes it read as a form
   rather than a page with inputs in it. */
.auth-col { width: 100%; max-width: 26rem; }

.auth-index {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: var(--s-3);
}

.auth-index .tick {
  flex: none;
  width: 26px;
  height: 1px;
  background: var(--rule-ink);
}

/* The second label is usually an email address, which can be longer than the
   column. Truncating beats wrapping under the rule. */
.auth-index .micro:last-child {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.auth-title { text-wrap: balance; }

.auth-lead {
  margin-top: var(--s-2);
  font-size: var(--t-body);
  text-wrap: pretty;
}

.auth-body { margin-top: var(--s-4); }

.auth-error {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  margin-bottom: var(--s-3);
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--c-pdf);
  border-radius: var(--r-control);
  /* A tint of the same hue rather than a red fill: this is a correction, not
     an alarm, and most of the time it says "that password was wrong". */
  background: color-mix(in srgb, var(--c-pdf) 6%, var(--paper-raise));
  color: var(--ink);
  font-size: var(--t-body-s);
  line-height: 1.5;
}

.auth-error svg {
  flex: none;
  width: 17px;
  height: 17px;
  margin-top: 0.15rem;
  color: var(--c-pdf);
}

/* --- Providers ------------------------------------------------------------ */

.auth-providers { display: grid; gap: 0.6rem; }

.auth-provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.65rem;
  height: 48px;
  padding-inline: 1rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-control);
  background: var(--paper-raise);
  font-size: var(--t-body-s);
  font-weight: 500;
  letter-spacing: -0.008em;
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease);
}

.auth-provider:hover {
  border-color: var(--rule-ink);
  background: var(--paper-alt);
}

/* Opting the brand marks out of the reset's max-width clamp, for the reason
   .btn svg does: inside a shrink-to-fit track the clamp resolves smaller than
   the icon. */
.auth-provider svg { width: 18px; height: 18px; flex: none; max-width: none; }

.auth-divider {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.9rem;
  margin-block: var(--s-3);
}

.auth-divider::before,
.auth-divider::after {
  content: '';
  height: 1px;
  background: var(--rule);
}

.auth-divider span {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--ink-3);
}

/* --- The form ------------------------------------------------------------- */

.auth-form { display: grid; gap: var(--s-3); }

.auth-form label {
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--ink-2);
}

/* Label on the left, "Forgot?" on the right, sharing the baseline. */
.field-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
}

.auth-inline-link {
  font-size: var(--t-small);
  color: var(--ink-3);
  text-decoration: underline;
  text-decoration-color: var(--rule-ink);
  text-underline-offset: 3px;
}

.auth-inline-link:hover { color: var(--accent); text-decoration-color: currentColor; }

.field-hint {
  font-size: var(--t-small);
  color: var(--ink-3);
  line-height: 1.5;
}

/* 44px tall on a form, against the 15px used inside tool workbenches. A field
   somebody types a password into on a phone is not a settings control. */
.auth-form .input { height: 46px; font-size: var(--t-body-s); }

/* Managed 'interaction-only' draws nothing for most visitors, so this box has
   no height until Cloudflare decides otherwise — hence no margin of its own. */
.auth-challenge:not(:empty) { display: grid; }

.auth-submit { width: 100%; }

/* Held until the challenge settles. Both states say why rather than just going
   grey — a control disabled with no explanation reads as broken software. */
.auth-submit[disabled] { opacity: 0.55; cursor: progress; }
.auth-submit[data-busy='true'] { cursor: progress; }

.auth-alt {
  margin-top: var(--s-3);
  font-size: var(--t-body-s);
  color: var(--ink-2);
  text-align: center;
}

.auth-alt a {
  color: var(--ink);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: var(--rule-ink);
  text-underline-offset: 3px;
}

.auth-alt a:hover { color: var(--accent); text-decoration-color: currentColor; }

.auth-alt-quiet { color: var(--ink-3); font-size: var(--t-small); }

.auth-note {
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule);
  display: grid;
  gap: 0.7rem;
  font-size: var(--t-small);
  color: var(--ink-3);
  line-height: 1.6;
}

.auth-note strong { color: var(--ink-2); font-weight: 600; }

.auth-email { font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }

.auth-figure {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  margin: 0 auto var(--s-3);
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: var(--paper-raise);
  color: var(--accent);
}

.auth-figure svg { width: 24px; height: 24px; }

.auth-figure-spin {
  animation: auth-pulse 1.4s var(--ease) infinite;
}

@keyframes auth-pulse {
  0%, 100% { border-color: var(--rule); }
  50%      { border-color: var(--accent); }
}

/* --- Account page --------------------------------------------------------- */

.auth-facts {
  display: grid;
  gap: 0;
  margin: 0;
  border-top: 1px solid var(--rule);
}

.auth-facts > div {
  display: grid;
  grid-template-columns: 8rem minmax(0, 1fr);
  gap: var(--s-2);
  align-items: baseline;
  padding-block: 0.8rem;
  border-bottom: 1px solid var(--rule);
}

.auth-facts dt { padding-top: 0.2rem; }

.auth-facts dd {
  margin: 0;
  font-size: var(--t-body-s);
  overflow-wrap: anywhere;
}

.auth-signout { margin-top: var(--s-3); }

.auth-history { margin-top: var(--s-6); }

/* The shared .section-head is a two-column grid sized for a full-width page;
   in a 26rem column its meta cell crushes the title. */
.auth-history-head { margin-bottom: var(--s-3); }
.auth-history-head .micro { margin-bottom: 0.5rem; }

.history-list {
  border-top: 1px solid var(--rule);
}

.history-row {
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 1fr) auto;
  gap: var(--s-2);
  align-items: baseline;
  padding-block: 0.75rem;
  border-bottom: 1px solid var(--rule);
}

.history-idx { padding-top: 0.15rem; color: var(--ink-4); }

.history-main { min-width: 0; }

.history-tool {
  display: block;
  font-size: var(--t-body-s);
  font-weight: 500;
  letter-spacing: -0.01em;
}

a.history-tool:hover { color: var(--accent); }

/* A tool that has since been retired keeps its rows but has nowhere to link. */
.history-tool-gone { color: var(--ink-3); }

.history-file {
  display: block;
  font-size: var(--t-small);
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.history-meta {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  font-size: var(--t-micro);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.history-empty {
  padding-block: var(--s-3);
  border-block: 1px solid var(--rule);
}

.auth-history-clear { margin-top: var(--s-3); }

/* --- Masthead account link ------------------------------------------------ */

.masthead-end {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.masthead-account {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  height: 36px;
  padding-inline: 0.65rem;
  border-radius: var(--r-control);
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink-2);
  transition: color 0.18s var(--ease), background 0.18s var(--ease);
}

.masthead-account:hover { color: var(--ink); background: var(--paper-alt); }
.masthead-account svg { flex: none; color: var(--ink-4); }
.masthead-account:hover svg { color: var(--accent); }

/* --- Load choreography ---------------------------------------------------- */

.reveal {
  opacity: 0;
  transform: translateY(10px);
  animation: reveal 0.7s var(--ease) forwards;
}

@keyframes reveal {
  to { opacity: 1; transform: none; }
}

.reveal-1 { animation-delay: 0.04s; }
.reveal-2 { animation-delay: 0.10s; }
.reveal-3 { animation-delay: 0.16s; }
.reveal-4 { animation-delay: 0.24s; }
.reveal-5 { animation-delay: 0.32s; }

/* --- Leak Check ----------------------------------------------------------- */

/* Two registers on one page, and they must not be confusable. A finding is a
   fact about the bytes — plain, dark, stated. A verdict is a model's reading of
   that fact, and it is set in the quieter tone with a rule beside it, because a
   page that renders an opinion exactly like a measurement has spent the
   credibility of every honest row on it. */

.leak-verdict-line {
  margin: 0;
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-h3);
  letter-spacing: -0.02em;
  color: var(--ink);
}

.leak-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.leak-row {
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.leak-head {
  display: flex;
  gap: var(--s-2);
  align-items: baseline;
  justify-content: space-between;
}

.leak-label {
  font-size: var(--t-body-s);
  font-weight: 600;
  color: var(--ink);
}

.leak-where {
  margin-left: auto;
  font-size: var(--t-small);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The severity chip, and the reason it is outlined rather than filled.
   Everything else on this row is a fact about the bytes; this is a model's
   opinion of that fact, and a solid badge would carry more authority than the
   thing it is describing. Outlined, in the row's own colour, reading as an
   annotation — but visible, because the first version put the whole verdict in
   the quiet register and nobody could tell the button had done anything. */
.leak-severity {
  padding: 0.16rem 0.42rem;
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  color: var(--ink-2);
  white-space: nowrap;
}

/* The recovered text itself. Monospaced because it is evidence rather than
   prose — and because a Windows path and a name read very differently when
   they are set in the same face as the sentence describing them. */
.leak-value {
  margin: 0.45rem 0 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--t-small);
  line-height: 1.55;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.leak-detail {
  margin: 0.45rem 0 0;
  font-size: var(--t-small);
  color: var(--ink-3);
  max-width: 68ch;
}

.leak-verdict {
  margin: 0.6rem 0 0;
  padding-left: 0.7rem;
  border-left: 2px solid var(--rule-strong);
  font-size: var(--t-small);
  color: var(--ink-2);
  max-width: 68ch;
}

.leak-row[data-severity='high'] .leak-label,
.leak-row[data-severity='high'] .leak-severity { color: var(--flag); }
.leak-row[data-severity='high'] .leak-severity { border-color: var(--flag); }
.leak-row[data-severity='high'] .leak-verdict { border-left-color: var(--flag); }

.leak-row[data-severity='medium'] .leak-severity { color: var(--accent); border-color: var(--accent); }
.leak-row[data-severity='medium'] .leak-verdict { border-left-color: var(--accent); }

/* Dimmed, not hidden. A model calling a finding harmless is still a model's
   opinion, and the row underneath it is a fact the visitor may disagree about. */
.leak-row[data-severity='none'] .leak-severity { color: var(--ink-3); }
.leak-row[data-severity='none'] .leak-label { color: var(--ink-2); }

/* Never sent for an opinion, as opposed to judged and found harmless. Dashed,
   so it does not read as a verdict at all. */
.leak-severity[data-unsent='true'] {
  border-style: dashed;
  color: var(--ink-3);
}

/* Both of these are hidden until the tool says otherwise, so the base rule
   carries `display: none` and the state selector below turns it on. Declaring
   the visible form first and hiding it afterwards works only by specificity,
   which is a fragile way to say something this simple. */
.leak-empty {
  display: none;
  margin: 0;
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
  font-size: var(--t-body-s);
  color: var(--ink-2);
  max-width: 64ch;
}

.leak-note {
  margin: 0;
  padding: 0 var(--s-3) var(--s-3);
  font-size: var(--t-small);
  color: var(--ink-3);
}

.leak-thumbs {
  display: none;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-3);
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.leak-thumbs img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--paper);
}

.leak-thumbs figcaption { margin-top: 0.4rem; }

.leak-receipt {
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.leak-receipt-list {
  margin: 0.5rem 0 0;
  padding-left: 1.1rem;
  font-size: var(--t-small);
  color: var(--ink-2);
  max-width: 72ch;
}

.leak-receipt-list li { margin-bottom: 0.3rem; }

/* What was deliberately not done. Same list, different colour, never omitted —
   a receipt that lists only the successes is an advertisement. */
.leak-receipt-list li[data-state='left'] { color: var(--ink-3); }

.leak-pages {
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

/* These three hold nothing until the tool puts something in them, and an empty
   block with padding is a gap nobody can explain. After their base rules, so
   the override reads as one rather than relying on the pseudo-class winning. */
.leak-note:empty,
.leak-receipt:empty,
.leak-pages:empty { display: none; }

.leak-page {
  flex: none;
  width: 112px;
  margin: 0;
}

.leak-page img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
}

.leak-page[data-flagged='true'] img { border-color: var(--flag); }
.leak-page[data-flagged='true'] figcaption { color: var(--flag); }

.leak-notes {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-3);
  padding: var(--s-3);
}

.leak-note-list {
  margin: 0.5rem 0 0;
  padding-left: 1.1rem;
  font-size: var(--t-small);
  color: var(--ink-3);
}

.leak-note-list li { margin-bottom: 0.25rem; }

.workbench[data-has-thumb='true'] .leak-thumbs { display: grid; }
.workbench[data-found='no'] .leak-empty { display: block; }

/* --- Screenshot Check ----------------------------------------------------- */

/* Screenshot Check is "Leak Check for pixels": same findings-list structure,
   plus a canvas for the image and redaction tools. The verdict, list, notes,
   and controls mirror the leak-check patterns. */

.check-verdict-line {
  margin: 0 0 var(--s-3);
  font-size: var(--t-body);
  color: var(--ink-2);
  text-align: center;
}

.check-stage {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0 0 var(--s-3);
  padding: var(--s-2);
  background: var(--paper-alt);
  border-radius: var(--r-card);
  overflow: hidden;
}

.check-stage canvas {
  max-width: 100%;
  max-height: 60vh;
  border-radius: calc(var(--r-card) - 2px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.check-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.check-draw-box {
  position: absolute;
  border: 2px dashed var(--accent);
  background: rgba(0, 0, 0, 0.1);
  pointer-events: none;
}

.check-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.check-row {
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--rule);
}

.check-row:last-child { border-bottom: none; }

.check-head {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  flex-wrap: wrap;
}

.check-label {
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.check-where {
  font-size: var(--t-small);
  color: var(--ink-3);
}

.check-value {
  margin: 0.35rem 0 0;
  font-size: var(--t-body);
  color: var(--ink-2);
  word-break: break-word;
}

.check-detail {
  margin: 0.35rem 0 0;
  font-size: var(--t-small);
  color: var(--ink-3);
}

.check-redact-btn {
  margin-top: 0.5rem;
  padding: 0.25rem 0.75rem;
  font-size: var(--t-small);
  color: var(--ink-2);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.check-redact-btn:hover:not(:disabled) {
  background: var(--paper-alt);
  border-color: var(--ink-4);
}

.check-redact-btn:disabled {
  color: var(--ink-3);
  cursor: default;
}

.check-row[data-redacted='true'] .check-label {
  color: var(--ink-3);
  text-decoration: line-through;
}

.check-empty {
  display: none;
  margin: var(--s-3) 0;
  font-size: var(--t-body);
  color: var(--ink-3);
  text-align: center;
}

.check-redact-controls {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: var(--s-3) 0;
  padding: var(--s-2);
  background: var(--paper-alt);
  border-radius: var(--r-card);
}

.check-redact-controls .micro {
  margin-right: auto;
}

.check-notes {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-3);
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule);
}

.check-note-list {
  list-style: disc;
  margin: 0.5rem 0 0;
  padding-left: 1.1rem;
  font-size: var(--t-small);
  color: var(--ink-3);
}

.check-note-list li { margin-bottom: 0.25rem; }

.workbench[data-found='no'] .check-empty { display: block; }

@media (max-width: 640px) {
  .check-notes { grid-template-columns: 1fr; }
  .check-head { flex-direction: column; gap: 0.15rem; }
  .check-redact-controls { flex-wrap: wrap; }
}

/* --- Explain This --------------------------------------------------------- */

/* A long result read top to bottom, so the structure is carried by the same
   hairline rules as everything else rather than by cards. The one thing that
   is *not* quiet is the confidence block: it sits in tinted paper at the
   bottom because the failure this tool has is a confident summary of a
   document it half-read, and a caveat set in --ink-4 at the end is a caveat
   nobody reads. */

.explain-hint {
  max-width: 46ch;
  margin: 0.35rem auto 1.5rem;
}

.explain-head {
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.explain-title {
  margin: 0;
  font-size: var(--t-h3);
  letter-spacing: -0.02em;
  color: var(--ink);
}

.explain-meta {
  display: flex;
  gap: var(--s-2);
  margin: 0.4rem 0 0;
}

.explain-layout {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--s-3);
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.explain-source img,
.explain-source .explain-excerpt { display: none; }

.workbench[data-preview='image'] .explain-source img { display: block; }
.workbench[data-preview='text'] .explain-source .explain-excerpt { display: block; }
.workbench[data-preview='none'] .explain-layout { grid-template-columns: 1fr; }
.workbench[data-preview='none'] .explain-source { display: none; }

.explain-source img {
  width: 100%;
  height: auto;
  border: 1px solid var(--rule);
  background: #fff;
}

/* The opening of a text file, as it is. Monospaced and clipped rather than
   wrapped-and-endless: this is here so the reader can confirm we are talking
   about the file they meant, not so they can read it here. */
.explain-excerpt {
  max-height: 340px;
  margin: 0;
  padding: var(--s-2);
  overflow: auto;
  border: 1px solid var(--rule);
  background: #fff;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--t-micro);
  line-height: 1.6;
  color: var(--ink-2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.explain-source .micro { display: block; margin-top: 0.4rem; }

.explain-lede {
  margin: 0;
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--ink);
  max-width: 64ch;
}

.explain-facts {
  display: grid;
  gap: var(--s-2);
  margin: var(--s-3) 0 0;
}

.explain-facts dt { margin-bottom: 0.2rem; }

.explain-facts dd {
  margin: 0;
  font-size: var(--t-body-s);
  color: var(--ink-2);
  max-width: 64ch;
}

.explain-section,
.explain-confidence,
.explain-ask {
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

/* A heading with nothing under it is a claim. "Worth knowing before you act"
   above an empty list reads as a clean bill of health, and this tool is never
   entitled to give one — so the section removes itself instead. */
.explain-section[data-empty] { display: none; }

.explain-micro-flag { color: var(--flag); }

.explain-list {
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0;
}

.explain-item {
  padding: 0.7rem 0;
  border-top: 1px solid var(--rule);
  font-size: var(--t-body-s);
  color: var(--ink-2);
  max-width: 74ch;
}

.explain-item:first-child { border-top: 0; padding-top: 0.2rem; }

.explain-item-head {
  display: flex;
  gap: var(--s-2);
  align-items: baseline;
  justify-content: space-between;
}

.explain-item-label {
  font-weight: 600;
  color: var(--ink);
}

.explain-item-where {
  font-size: var(--t-small);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.explain-item-detail { margin: 0.3rem 0 0; }

.explain-item[data-importance='high'] .explain-item-label { color: var(--flag); }

.explain-confidence { background: var(--accent-tint); }

.explain-confidence-line {
  margin: 0.5rem 0 0;
  font-size: var(--t-body-s);
  color: var(--ink-2);
  max-width: 74ch;
}

.explain-confidence-line strong {
  text-transform: capitalize;
  color: var(--ink);
}

.explain-unknowns .explain-item { border-top-color: var(--rule-strong); }

.explain-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.6rem 0 0;
}

.explain-thread {
  list-style: none;
  margin: var(--s-3) 0 0;
  padding: 0;
}

.explain-thread:empty { margin: 0; }

/* The question is set as the reader's own words and the answer in the quieter
   tone with a rule beside it — the same two registers Leak Check uses, for the
   same reason: a page that renders a model's answer identically to the words
   you typed has blurred which of the two you can rely on. */
.explain-turn-question {
  margin-top: var(--s-2);
  font-size: var(--t-body-s);
  font-weight: 600;
  color: var(--ink);
}

.explain-turn-answer {
  margin-top: 0.4rem;
  padding-left: 0.7rem;
  border-left: 2px solid var(--rule-strong);
  font-size: var(--t-body-s);
  line-height: 1.6;
  color: var(--ink-2);
  max-width: 74ch;
  white-space: pre-wrap;
}

.explain-form {
  display: flex;
  gap: 0.4rem;
  margin-top: var(--s-3);
}

.explain-input { flex: 1; min-width: 0; }

.explain-note {
  margin: 0.5rem 0 0;
  font-size: var(--t-small);
  color: var(--ink-3);
  max-width: 74ch;
}

.explain-note:empty { display: none; }

@media (max-width: 720px) {
  /* The preview goes under the explanation rather than beside it, and shrinks:
     on a phone it is confirmation that the right file went in, and the answer
     is what the visitor came for. */
  .explain-layout { grid-template-columns: 1fr; }
  .explain-source { order: 2; }
  .explain-source img, .explain-excerpt { max-height: 220px; }
  .explain-source img { width: auto; max-width: 100%; }
  .explain-form { flex-wrap: wrap; }
  .explain-form .btn { width: 100%; justify-content: center; }
}

/* --- Blog ------------------------------------------------------------------
   The listing, the post, and the cards both of them use.

   Nothing here invents a look: the cards are the ruled grid from `.tool-grid`,
   the chips are the homepage's chips, the body is `.article` with the elements
   prose needs that a catalog page never had (lists, quotes, tables, code,
   figures). The one thing the blog owns outright is the post body, because it
   is the only place on the site where the HTML is written elsewhere. */

.post-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-bottom: var(--s-4);
}

/* Declared after `.chip` — it overrides three of its properties, and on equal
   specificity that only works below it. See tests/test_stylesheet.py. */
.chip-on {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}

.chip-on:hover {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}

/* The ruled grid, but drawn by the cards rather than by the gaps.
   `.tool-grid` paints the container `--rule` and lets 1px gaps show through,
   which works because a catalog row is always full. A blog listing is whatever
   number of posts exists, so four posts in a three-column grid left two empty
   cells showing as grey blocks. Each card carries its own hairline as a
   box-shadow instead: adjacent shadows land in the same 1px gap and read as one
   rule, and an empty cell is simply paper. */
/* The listing needs air under it before the footer; the related strip does not,
   because the band it sits in supplies its own. */
.post-list { padding-bottom: var(--s-6); }

.post-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--paper);
  border-radius: var(--r-card);
  overflow: hidden;
}

.post-card {
  background: var(--paper);
  box-shadow: 0 0 0 1px var(--rule);
  display: flex;
  flex-direction: column;
  transition: background 0.22s var(--ease);
}

.post-card:hover { background: var(--paper-raise); }

/* Fixed aspect rather than the image's own: post images arrive from wherever
   the writer got them, and a grid whose rows are as tall as the tallest photo
   somebody uploaded is not a grid. */
.post-card-shot {
  aspect-ratio: 16 / 9;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--paper-alt);
  border-bottom: 1px solid var(--rule);
  color: var(--rule-ink);
  overflow: hidden;
}

.post-card-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.4s var(--ease);
}

.post-card:hover .post-card-shot img { transform: scale(1.03); }

.post-card-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: var(--s-3);
  flex: 1;
}

.post-card-meta {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--ink-3);
}

.post-card-title {
  font-size: var(--t-h3);
  font-weight: 500;
  letter-spacing: -0.018em;
  line-height: 1.24;
}

.post-card-desc {
  font-size: var(--t-body-s);
  color: var(--ink-3);
  line-height: 1.6;
  /* Three lines, then stop. Descriptions are written to a length nobody
     enforces, and one long one used to set the height of its whole row. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.post-card-go {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: auto;
  padding-top: var(--s-2);
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink-3);
  transition: color 0.2s var(--ease);
}

.post-card-go svg { flex: none; transform: translateX(-3px); transition: transform 0.22s var(--ease); }
.post-card:hover .post-card-go { color: var(--accent); }
.post-card:hover .post-card-go svg { transform: translateX(0); }

.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule);
}

.pager-step {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--t-body-s);
  font-weight: 500;
  color: var(--ink-2);
  transition: color 0.18s var(--ease);
}

.pager-step:hover { color: var(--accent); }

/* The ends of the range are rendered as spans, not links — a disabled anchor
   is still focusable and still looks clickable. */
.pager-step-off { color: var(--ink-4); }

/* ── The post ─────────────────────────────────────────────────────────────
   Narrower than the tool pages on purpose: 68ch is the measure `.article`
   already sets, and a post is nothing but measure. */

.post { max-width: 72ch; }

/* Same block rhythm as `.tool-head`, which every other page under the crumbs
   uses — without the top padding the index line sits on the breadcrumb. */
.post-head { padding-block: var(--s-4); }

.post-head h1 {
  font-size: var(--t-h2);
  font-weight: 500;
  letter-spacing: -0.026em;
  line-height: 1.1;
  margin-block: var(--s-2) var(--s-2);
}

.post-byline {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: var(--s-3);
}

.post-figure {
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  overflow: hidden;
  margin-bottom: var(--s-4);
}

.post-figure img { width: 100%; display: block; }

/* ── Post body ────────────────────────────────────────────────────────────
   `.article` gives headings, paragraphs and the measure. Everything below is
   what prose from outside the codebase brings with it and a catalog page never
   did. It is styled here rather than left to the browser because the defaults
   are Times-ish, full-width and out of the system. */

.post-body > *:first-child { margin-top: 0; }

.post-body h2 { margin-top: var(--s-5); }
.post-body h3 { margin-top: var(--s-4); font-size: var(--t-lead); }

.post-body a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 0.16em;
  text-decoration-thickness: 1px;
}

.post-body a:hover { color: var(--accent-deep); }
.post-body strong { color: var(--ink); font-weight: 600; }

.post-body ul, .post-body ol {
  color: var(--ink-2);
  margin-bottom: var(--s-2);
  padding-left: 1.25rem;
  line-height: 1.68;
}

.post-body ul { list-style: disc; }
.post-body ol { list-style: decimal; }
.post-body li { margin-bottom: 0.4rem; }
.post-body li::marker { color: var(--ink-4); }

.post-body blockquote {
  border-left: 2px solid var(--accent);
  padding: 0.35rem 0 0.35rem var(--s-3);
  margin-bottom: var(--s-3);
  color: var(--ink-2);
  font-size: var(--t-lead);
  line-height: 1.6;
}

.post-body blockquote p:last-child { margin-bottom: 0; }

.post-body hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin-block: var(--s-4);
}

.post-body img {
  width: 100%;
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  margin-bottom: var(--s-3);
}

.post-body figure { margin-bottom: var(--s-3); }
.post-body figcaption {
  font-size: var(--t-small);
  color: var(--ink-3);
  margin-top: -0.5rem;
}

/* A wide table cannot be made to fit a 72ch measure, so it scrolls inside its
   own box rather than pushing the page sideways. */
.post-body table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-body-s);
  margin-bottom: var(--s-3);
  display: block;
  overflow-x: auto;
}

.post-body th, .post-body td {
  text-align: left;
  padding: 0.7rem 0.9rem;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}

.post-body th {
  font-size: var(--t-micro);
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-3);
  border-bottom-color: var(--rule-strong);
}

.post-body td { color: var(--ink-2); }

.post-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.875em;
  background: var(--paper-alt);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 0.1rem 0.3rem;
}

.post-body pre {
  background: var(--paper-alt);
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  padding: var(--s-2);
  overflow-x: auto;
  margin-bottom: var(--s-3);
}

.post-body pre code { background: none; border: 0; padding: 0; font-size: var(--t-small); }

/* The bottom margin is not decoration: `.page` has no bottom padding, so the
   buttons sat directly on the band that follows, boxes touching a rule. */
.post-foot {
  margin-top: var(--s-5);
  margin-bottom: var(--s-5);
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem 1rem;
}

.post-share {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* ── Chat Catch-Up ─────────────────────────────────────────────────────── */
/* Two surfaces this tool owns. The strip, which is the visitor deciding what
   order the conversation happened in — a control, not a preview, which is why
   it sits outside the dropzone and survives the state change. And the stage,
   which holds a canvas rather than an <img>: the card is painted, so the
   themes are drawing rather than CSS and nothing here styles the card itself.
   See runtime/card.js for why it is painted at all. */

/* `display` beats the user agent's `[hidden] { display: none }`, so anything
   given a display value here has to say what `hidden` means for it. The site
   already does this per component — `.menu-row[hidden]`, `.install[hidden]`,
   `.nav-fav[hidden]` — and this pair was missed: the strip shipped visible and
   empty before a single screenshot was dropped, which reads as a broken panel.
   The browser smoke test asserts the art panel is hidden signed out, which is
   what caught it. */
.shot-strip[hidden], .card-art[hidden] { display: none; }

.shot-strip {
  border-top: 1px solid var(--rule);
  padding: var(--s-3);
  display: grid;
  gap: var(--s-2);
}

.shot-strip-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
}

.shot-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  /* The strip is the one horizontally scrolling surface on the site, and on a
     phone six thumbnails will not fit any other way. */
  scroll-snap-type: x proximity;
  padding-bottom: 0.35rem;
}

.shot-item {
  flex: none;
  width: 8.5rem;
  scroll-snap-align: start;
  position: relative;
  display: grid;
  gap: 0.35rem;
  padding: 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--paper-raise);
}

.shot-item img {
  width: 100%;
  height: 9rem;
  object-fit: cover;
  object-position: top;
  border-radius: 3px;
  background: var(--paper-alt);
}

/* The index, which is the whole point of the strip: it is the order the
   conversation is claimed to have happened in. */
.shot-number {
  position: absolute;
  top: 0.85rem;
  left: 0.85rem;
  z-index: 1;
  min-width: 1.4rem;
  height: 1.4rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--ink);
  color: var(--paper);
  font-size: var(--t-micro);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.shot-move { display: flex; gap: 0.25rem; justify-content: center; }

.shot-btn {
  flex: none;
  width: 1.9rem;
  height: 1.9rem;
  display: grid;
  place-items: center;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--paper);
  color: var(--ink-2);
  font-size: var(--t-small);
  line-height: 1;
  cursor: pointer;
}

.shot-btn:hover:not(:disabled) { border-color: var(--rule-ink); color: var(--ink); }
.shot-btn:disabled { color: var(--ink-4); cursor: default; }

.card-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 17rem;
  gap: var(--s-3);
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.card-stage {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: var(--s-3);
  background: var(--paper-alt);
  border-radius: 6px;
}

/* A 9:16 frame, because that is what the card is now — a thing you post, not
   a document that grows with its content. Bounded by height rather than width
   so the preview stays phone-shaped at any column width. */
.card-stage canvas {
  max-width: 100%;
  max-height: 38rem;
  height: auto;
  border-radius: 14px;
  box-shadow: var(--lift-strong);
}

.card-side { display: grid; gap: var(--s-2); align-content: start; }

.theme-row { display: flex; flex-wrap: wrap; gap: 0.35rem; }

/* Each chip previews its own theme rather than describing it, which is the
   only way to choose between eight looks without pressing all of them. The
   Poster chip carries no custom properties and falls back to the page's own
   colours, because that is exactly what it paints. */
.theme-chip {
  padding: 0.34rem 0.68rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  /* The chip previews the theme's real ground rather than a flat swatch:
     choosing between eight looks by reading eight words is not choosing. */
  background: linear-gradient(135deg,
    var(--chip-paper, var(--paper)), var(--chip-to, var(--paper)));
  color: var(--chip-ink, var(--ink-2));
  font-size: var(--t-micro);
  font-weight: 600;
  cursor: pointer;
}

.theme-chip:hover { border-color: var(--rule-ink); }
.theme-chip[aria-pressed="true"] { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }

.card-toggle {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--t-body-s);
  color: var(--ink-2);
  cursor: pointer;
}

.card-toggle-note { font-size: var(--t-micro); margin: -0.35rem 0 0; }

.card-editor { padding: var(--s-3); display: grid; gap: var(--s-3); }

.card-block { display: grid; gap: 0.4rem; }

.card-row { display: flex; gap: 0.4rem; align-items: center; }

.card-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--paper-raise);
  color: var(--ink);
  font: inherit;
  font-size: var(--t-body-s);
}

.card-input:focus { outline: none; border-color: var(--accent); }

.card-input-narrow { flex: 0 0 9rem; }

/* Generated card art. Hidden outright for a signed-out visitor rather than
   shown disabled — an account is required because a picture costs about a
   hundred times a text call, and a dead button is worse than no button. */

.card-art {
  display: grid;
  gap: 0.4rem;
  padding: var(--s-2);
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--paper-raise);
}

/* Wider than one line on purpose: this field is the privacy claim made
   checkable, so the whole sentence has to be readable without scrolling it. */
.card-brief {
  resize: vertical;
  min-height: 4.6rem;
  line-height: 1.4;
}

/* Empty until something happens, and it must not hold a line of space open
   while it is: a gap that appears and disappears under a button makes the
   panel jump every time art is generated. */
.card-art-note { font-size: var(--t-micro); margin: 0; }
.card-art-note:empty { display: none; }

/* `.editor-actions` is a flex row, which is right beside a 40rem editor and
   wrong in this 17rem column: every label broke mid-word — "Generate / art",
   "Copy / image", "Download / PNG" — which reads as a broken layout rather
   than a tight one. Buttons here fill the width and wrap as whole buttons. */
.card-art-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.card-art-actions .btn {
  flex: 1 1 auto;
  justify-content: center;
  white-space: nowrap;
}

/* --- Handoffs ------------------------------------------------------------- */

/* Deliberately quieter than the tool above it. This is the end of a page whose
   job is already done, and a card competing with the result for attention is
   the advertisement PRODUCT_SPEC.md forbids. Paper, hairlines, one accent on
   the label and nothing else. */

.handoffs {
  display: grid;
  gap: var(--s-2);
  margin-top: var(--s-4);
}

/* `display` above means this is mandatory. Without it, `hidden` does nothing
   and every tool page with a handoff shows an empty two-column box before
   anybody has uploaded anything — visible, wrong, and throwing no error. */
.handoffs[hidden] { display: none; }

.handoff-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  overflow: hidden;
}

/* One handoff should not stretch across two columns and read as a banner. */
.handoff-list:has(.handoff:only-child) { grid-template-columns: minmax(0, 34rem); }

.handoff {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.45rem;
  padding: var(--s-3);
  background: var(--paper);
}

/* The product's own icon and its name, so "more from us" says who us is
   without the visitor having to read a paragraph to find out. The marks are
   the app icons themselves — see the sprite in base.html for why they are not
   redrawn on the house grid — which makes this the only colour on the card,
   and the reason the tile stays at icon size and the name stays in micro
   caps: a logo lockup here would be the advertisement the spec forbids. */
.handoff-brand {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin: 0;
}

/* `max-width: 100%` from the reset collapses a sprite <svg> inside a
   shrink-to-fit box — the same trap `.btn svg` opts out of, and this row is
   shrink-to-fit because the card is `align-items: flex-start`. */
.handoff-mark { width: 28px; height: 28px; flex: none; max-width: none; }

.handoff-heading {
  margin: 0;
  font-size: var(--t-h3);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--ink);
  line-height: 1.2;
}

.handoff-body {
  margin: 0;
  font-size: var(--t-body-s);
  color: var(--ink-2);
  line-height: 1.6;
}

.handoff-note {
  margin: 0;
  font-size: var(--t-small);
  color: var(--ink-3);
  line-height: 1.5;
}

/* The CTA sits on the bottom edge of the card, so two cards side by side line
   their buttons up whatever their prose does — and it needs clearance above
   it, which is a *margin* on a `.btn` and never a padding. A `.btn` is a fixed
   44px box: `padding-top` there does not move the button down the card at all,
   it moves the label down inside the button, and the label was sitting 4px
   below its own centre on every handoff on the site.
   One margin cannot be both `auto` and a length, so the push to the bottom is
   the last paragraph's `margin-bottom: auto` and the clearance is this one. */
.handoff-note, .handoff-body:last-of-type { margin-bottom: auto; }
.handoff-cta { margin-top: 0.55rem; }

/* --- Share a result ------------------------------------------------------- */

/* The one thing on this site that uploads what you made, so it is the quietest
   control on the page: a hairline row under the privacy strip, the same weight
   as the keyboard-shortcuts line beside it. It opens into a panel that names
   the file and asks again — see templates/website/_share.html.

   Everything here has a `display` value and therefore its own `[hidden]` rule.
   `.share` ships hidden in the markup and is revealed by runtime/share.js when
   the workbench reaches its result state; the panels take turns. A missing
   `[hidden]` rule renders all four at once and throws nothing. See CLAUDE.md. */

.share {
  display: grid;
  gap: var(--s-2);
  margin-top: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  background: var(--paper);
}

.share[hidden] { display: none; }

.share-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  flex-wrap: wrap;
}

.share-row[hidden] { display: none; }

.share-panel {
  display: grid;
  gap: 0.6rem;
  max-width: 56ch;
}

.share-panel[hidden] { display: none; }

.share-lead {
  margin: 0;
  font-size: var(--t-body-s);
  color: var(--ink-2);
}

.share-terms {
  margin: 0;
  color: var(--ink-3);
}

.share-actions {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  flex-wrap: wrap;
  margin-top: 0.2rem;
}

.share-link-row {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  flex-wrap: wrap;
}

/* The field takes the space and the button keeps its size: a link is long and
   a wrapped "Copy link" on its own line is how a two-control row falls apart
   on a phone. */
.share-link {
  flex: 1 1 18rem;
  min-width: 0;
  font-size: var(--t-small);
}

.share-error {
  margin: 0;
  color: var(--flag);
}

.share-error:empty { display: none; }

/* A button that has to read as prose, because it sits inside a sentence of it.
   Not an <a>: deleting a link is not a navigation, and a link that does
   something when you middle-click it is a trap. */
.linklike {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  cursor: pointer;
}

.linklike:hover { color: var(--accent-deep); }

/* --- The shared result page (/s/<token>) ---------------------------------- */

/* Somebody arrives here from a message, having never heard of us, wanting one
   file. So this is shaped like a workbench in its result state and nothing
   else: the thing, and the way to save it. */

.share-view {
  border: 1px solid var(--rule);
  border-radius: var(--r-card);
  background: var(--paper-raise);
  overflow: hidden;
  margin-top: var(--s-3);
}

.share-body {
  display: grid;
  justify-items: center;
  gap: var(--s-2);
  padding: var(--s-3);
}

/* Bounded by the viewport rather than by the image: a shared photo is often
   the full-size one, and a result that needs scrolling to see is not a
   preview. */
.share-image {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;
  border-radius: var(--r-control);
}

/* Wrapped, never scrolled sideways. A <pre> of a CSV on a phone is otherwise a
   one-line page with a horizontal scrollbar. */
.share-text {
  width: 100%;
  margin: 0;
  max-height: 60vh;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-small);
  line-height: 1.55;
  color: var(--ink);
}

.share-blank {
  display: grid;
  justify-items: center;
  gap: var(--s-1);
  padding: var(--s-4) 0;
  text-align: center;
  color: var(--ink-3);
}

.share-note { margin: 0; text-align: center; }

.share-revoke { margin: 0; }

/* --- Receipt Scanner ------------------------------------------------------ */

/* The result is one document read two ways: the picture on the left, the
   fields on the right. It reuses .meta-layout and .data-grid; what it adds is
   the review panel and the totals block, both of which are receipt-shaped
   rather than grid-shaped. */

/* The way into the tool for somebody who has no receipt to hand — which is
   most people arriving from a search. Deliberately a link rather than a second
   button: the primary action is still theirs, and two buttons of equal weight
   in a dropzone is a choice nobody asked for.

   No `display` of its own, so it needs no `[hidden]` rule. See the note in
   CLAUDE.md about what that costs when it is forgotten. */
.dropzone-alt {
  margin: 0.9rem 0 0;
  font-size: var(--t-small);
  color: var(--ink-3);
}

.btn-link {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  color: var(--ink-2);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--rule-ink);
  text-underline-offset: 3px;
}

.btn-link:hover { color: var(--accent); text-decoration-color: currentColor; }

/* The pile, beside the picture. A row is a receipt: its filename, and then
   either what it says or why it does not. Rows are buttons because clicking
   one opens it in the editor to the right — the same editor, repainted, so
   there is one interface to keep working rather than a list view and a
   detail view that drift. */
.queue {
  display: grid;
  gap: 0.5rem;
  margin-bottom: var(--s-2);
}

.queue[hidden] { display: none; }

/* Twenty receipts do not fit, so the list scrolls, and the last visible row is
   cut in half — which is the ordinary affordance for "there is more below".

   The usual CSS-only scroll shadow (two `local` background layers covering two
   `scroll` ones, so a shadow shows only where there is more content) does not
   work here and is not worth reaching for: the rows are opaque, because the
   1px gaps between them are this list's background showing through, so every
   row paints over the shadows. Making it work would mean a JS scroll listener
   for a cosmetic cue. */
.queue-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--r-control);
  max-height: 18rem;
  overflow-y: auto;
}

.queue-row {
  display: grid;
  gap: 0.1rem;
  width: 100%;
  border: 0;
  padding: 0.5rem 0.7rem;
  background: var(--paper);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.queue-row:hover:not(:disabled) { background: var(--paper-alt); }

/* Reading, or already given up on. Not clickable, and it should look it. */
.queue-row:disabled { cursor: default; }
.queue-row[data-status='waiting'] .queue-name,
.queue-row[data-status='reading'] .queue-name { color: var(--ink-3); }

.queue-row[aria-current='true'] {
  background: var(--accent-tint);
  box-shadow: inset 2px 0 0 var(--accent);
}

.queue-name {
  font-size: var(--t-body-s);
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.queue-note {
  font-size: var(--t-small);
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A file that could not be read says so in the one colour this system uses
   for "a requirement was not met", rather than in a new red. */
.queue-row[data-status='failed'] .queue-note { color: var(--flag); }

.receipt-headline {
  margin: var(--s-2) 0 0;
  font-size: var(--t-h3);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--ink);
  line-height: 1.2;
}

.receipt-headline + .prose-note {
  margin: 0.2rem 0 0;
  font-variant-numeric: tabular-nums;
}

/* Present whether or not anything is wrong. A panel that disappears when the
   list is empty says "clean" without having been willing to write it down —
   the same reasoning as .explain-section[data-empty]. */
.receipt-review {
  display: grid;
  gap: 0.5rem;
  padding: var(--s-2) var(--s-3);
  border-top: 1px solid var(--rule);
  background: var(--paper-alt);
}

.workbench[data-review='flagged'] .receipt-review { background: #FBF1EF; }

.workbench[data-review='flagged'] [data-review-label] { color: var(--flag); }

.receipt-review-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.35rem;
  font-size: var(--t-body-s);
  color: var(--ink-2);
  line-height: 1.5;
}

/* The mark on the field itself. Dotted rather than filled: this is "have a
   look at this", not "this is wrong" — the tool cannot know which of the two
   numbers it compared is the misread one. */
.workbench .input[data-review-flag],
.data-grid input[data-review-flag] {
  border-bottom: 2px dotted var(--flag);
  background: #FBF1EF;
}

.receipt-fields {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  padding: var(--s-3);
  border-top: 1px solid var(--rule);
}

.receipt-items th { white-space: nowrap; }
.receipt-items th:first-child { min-width: 12rem; }

.receipt-remove { min-width: 0; width: 1%; }
.receipt-remove .btn { white-space: nowrap; }

.receipt-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-2);
  padding: var(--s-3);
  border-top: 1px solid var(--rule);
}

.receipt-totals {
  display: grid;
  gap: 0.35rem;
  margin: 0;
  margin-left: auto;
}

.receipt-total-row {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-2);
}

.receipt-total-row dd { margin: 0; }

.receipt-total-row input { text-align: right; font-variant-numeric: tabular-nums; }

/* The one figure anybody checks first, so it is the one set apart. */
.receipt-total-row-sum {
  padding-top: 0.45rem;
  border-top: 1px solid var(--rule-strong);
}

.receipt-total-row-sum input { font-weight: 600; color: var(--ink); }

/* --- Video to GIF --------------------------------------------------------- */
/* Two panes: the clip on the left, the GIF on the right, the same shape as the
   before-and-after the image tools use. It is not a comparison though — the
   left pane is a control. The video seeks to whichever trim handle was last
   moved, so the pair reads as "this much of that becomes this". */

.clip {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--rule);
  border-bottom: 1px solid var(--rule);
}

.clip-pane {
  background: var(--paper);
  padding: var(--s-3);
  min-width: 0;
}

.clip-media {
  display: block;
  width: 100%;
  max-height: 320px;
  margin-top: 0.6rem;
  object-fit: contain;
  border-radius: 10px;
  border: 1px solid var(--rule);
  /* A stage rather than the chequer the image tools use. Theirs means "this
     part is transparent", which is a real answer about a PNG and a wrong one
     here: a GIF made from video has no transparency in it, and the pane is
     letterboxed only because a 4:3 clip is not the shape of the column. The
     minimum height is for the moment before the first GIF exists, so the
     numbers under it do not start halfway up the panel and drop. */
  background: var(--paper-alt);
  min-height: 7rem;
}

/* Reading a clip is seconds of work, not milliseconds, and the workbench bar's
   hairline says only that something is happening. This says how much of it is
   done. Faded rather than hidden between runs: `visibility` and `opacity` cost
   nothing to toggle and leave the layout still, where a `display` toggle would
   move the numbers under it every time a slider stopped. */
.clip-bar {
  margin: 0 var(--s-3);
  height: 3px;
  border-radius: 2px;
  background: var(--rule);
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.2s var(--ease);
}

.workbench[data-working] .clip-bar { opacity: 1; }

.clip-bar span {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width 0.2s var(--ease);
}

/* Sits under the settings row and only when the frame rate had to come down,
   so it is a note about the controls above it rather than about the result. */
.clip-note {
  margin: 0;
  padding: 0 var(--s-3);
  max-width: 62ch;
}

@media (prefers-reduced-motion: reduce) {
  .clip-bar span { transition: none; }
}

@media (max-width: 46rem) {
  .clip { grid-template-columns: minmax(0, 1fr); }
}

/* --- Responsive ----------------------------------------------------------- */

@media (max-width: 1080px) {
  .cat-grid { grid-template-columns: repeat(3, 1fr); }
  .footer-grid { grid-template-columns: 1fr 1fr; row-gap: var(--s-4); }
  .hero-top { grid-template-columns: 1fr; }
  .hero-aside { display: none; }
}

@media (max-width: 900px) {
  .tool-grid { grid-template-columns: repeat(2, 1fr); }
  .post-grid { grid-template-columns: repeat(2, 1fr); }
  .tool-card, .tool-card--3, .tool-card--4 { grid-column: span 1; min-height: 190px; }
  .tool-card--4 {
    padding: 1.375rem 1.375rem 1.125rem;  /* match the others once stacked */
  }
  .tool-card--4 h3 { font-size: 1.0625rem; }
  .tool-card--4 p { font-size: var(--t-body-s); }
  .demo { grid-template-columns: 1fr; }
  .collections { grid-template-columns: 1fr; }
  .related { grid-template-columns: 1fr; }
  .principles { grid-template-columns: 1fr; }
  .principle {
    border-right: 0;
    border-bottom: 1px solid var(--rule);
    padding: var(--s-3) 0;
  }
  .principle:not(:first-child) { padding-left: 0; }
  .principle:last-child { border-bottom: 0; }
  .listing-row { grid-template-columns: 2.25rem minmax(0, 1fr) auto; }
  .listing-desc { display: none; }
}

@media (max-width: 720px) {
  :root { --gutter: 1.125rem; }
  .nav { display: none; }
  /* Both of these move into the menu, which is the point of it. The account
     link used to survive here as an unlabelled person icon — better than no
     way in at all, which was the alternative then — but a panel that can spell
     the word beats a glyph nobody has to guess at, and two icons in the
     masthead read more clearly than three. */
  .masthead-account { display: none; }
  .masthead-menu { display: inline-flex; }
  .auth-facts > div { grid-template-columns: 1fr; gap: 0.15rem; }
  .history-row { grid-template-columns: 1.5rem minmax(0, 1fr); }
  .history-meta { grid-column: 2; }
  .cat-grid { grid-template-columns: repeat(2, 1fr); }
  .tool-grid { grid-template-columns: 1fr; }
  .post-grid { grid-template-columns: 1fr; }
  .handoff-list { grid-template-columns: minmax(0, 1fr); }
  .receipt-foot { flex-direction: column; }
  .receipt-totals { margin-left: 0; width: 100%; }
  .receipt-total-row { justify-content: space-between; }
  .pager { flex-direction: column; align-items: flex-start; gap: 0.75rem; }
  .leak-notes { grid-template-columns: 1fr; }
  .leak-head { flex-direction: column; gap: 0.15rem; }
  .compare { grid-template-columns: 1fr; }
  /* The side column becomes a row above the card: at this width 17rem of
     controls beside a card leaves neither of them readable. */
  .card-layout { grid-template-columns: minmax(0, 1fr); }
  .card-stage canvas { max-height: 30rem; }
  .card-input-narrow { flex-basis: 6.5rem; }
  .search-shell { height: 60px; padding-left: 1rem; }
  .search-input { font-size: 1rem; }
  .search-key { display: none; }
  .search-go { width: 46px; height: 46px; }
  .search-result { grid-template-columns: 2rem minmax(0, 1fr); padding-inline: 1rem; }
  .search-result-cat { display: none; }
  .footer-grid { grid-template-columns: 1fr; }
}

/* --- Motion preferences --------------------------------------------------- */

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

/* ── AI Image Detector ─────────────────────────────────────────────────── */
/* The verdict is a number and a sentence, side by side, above the evidence
   that produced them. The number is coloured by the verdict on the section,
   not by its own value, so "hard to say" at 52% is grey and not orange. */

.aid-verdict {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-3);
  align-items: center;
  padding: var(--s-3);
  border-bottom: 1px solid var(--rule);
}

.aid-score {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  min-width: 6.5rem;
}

.aid-score-number {
  font-size: clamp(2.2rem, 5vw, 3.2rem);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
  transition: color 0.3s ease;
}

.workbench[data-verdict='ai'] .aid-score-number { color: var(--flag); }
.workbench[data-verdict='real'] .aid-score-number { color: var(--accent); }

.aid-headline {
  margin: 0;
  font-size: var(--t-h3);
  letter-spacing: -0.02em;
}

.aid-subline {
  margin: 0.3rem 0 0;
  font-size: var(--t-body-s);
  color: var(--ink-2);
}

.aid-report { margin-bottom: var(--s-3); }

.aid-report .report-row { grid-template-columns: 1.1rem minmax(0, 1fr); }

.aid-model { margin-top: var(--s-2); }

.aid-model-summary { margin-top: 0.5rem; }

.workbench[data-model-state='pending'] .aid-model-summary { color: var(--ink-3); }

.aid-model-foot {
  margin: 0;
  font-size: var(--t-small);
  color: var(--ink-3);
}

@media (max-width: 640px) {
  .aid-verdict { grid-template-columns: 1fr; }
}
