/* ═══════════════════════════════════════════════════════
   toolkit — dark theme
   Kanagawa Wave. Structurally identical to styles/light/style.css —
   only the palette block below differs between the two.
   ═══════════════════════════════════════════════════════ */

/* ── palette ─────────────────────────────────────────── */
:root {
  --sumiInk0:  #16161d;
  --sumiInk1:  #1f1f28;
  --sumiInk2:  #2a2a37;
  --sumiInk3:  #363646;
  --sumiInk4:  #54546d;
  --fujiWhite: #dcd7ba;
  --oldWhite:  #c8c093;
  --fujiGray:  #727169;
  --crystalBlue:  #7e9cd8;
  --springBlue:   #7fb4ca;
  --waveAqua:     #7aa89f;
  --springGreen:  #98bb6c;
  --carpYellow:   #e6c384;
  --roninYellow:  #ff9e3b;
  --surimiOrange: #ffa066;
  --sakuraPink:   #d27e99;
  --oniViolet:    #957fb8;
  --autumnRed:    #c34043;

  --mono: "JetBrains Mono", "Fira Code", "SF Mono", ui-monospace, Menlo, Consolas, monospace;

  color-scheme: dark;
}

* { box-sizing: border-box; }

/* ── native control appearance ───────────────────────── */
/* Every control here is a fully custom-styled <button>, so native widget
   rendering has nothing left to contribute. Standard practice, and each one
   already defines its own :focus-visible outline, so switching the native
   appearance off costs nothing.

   Deliberately NOT applied to <select>: appearance:none there also removes the
   dropdown arrow, which would need a custom replacement. Separate decision.

   Note for anyone measuring this. getComputedStyle().backgroundColor on a
   <button> does not reliably report the painted background. Firefox and Safari
   both returned native control colours for `.dropzone` — and *different* ones
   on consecutive runs, tracking pointer state — while the zone painted the
   author background correctly in both themes, confirmed by eye. Two rounds
   were lost chasing that as a CSS bug. Assert against a plain element carrying
   the same class instead; _dev-dropzone-harness.html does. */
button {
  -webkit-appearance: none;
  appearance: none;
}

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--sumiInk0);
  color: var(--fujiWhite);
  font-family: var(--mono);
  font-size: 14px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--springBlue); }

/* ── layout ──────────────────────────────────────────── */
.shell {
  display: grid;
  grid-template-columns: 168px minmax(0, 1fr) 168px;
  gap: 24px;
  max-width: 1440px;
  margin: 0 auto;
  padding: 24px 20px 48px;
}

.ad {
  position: sticky;
  top: 24px;
  align-self: start;
  height: 600px;
  border: 1px dashed var(--sumiInk4);
  border-radius: 6px;
  background:
    repeating-linear-gradient(135deg,
      transparent 0 9px,
      color-mix(in srgb, var(--sumiInk4) 13%, transparent) 9px 10px);
  color: var(--fujiGray);
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  display: flex;
  align-items: center;
  justify-content: center;
  writing-mode: vertical-rl;
  user-select: none;
}

/* Ads temporarily disabled site-wide (all three placeholder asides — both
   sticky rails and the mobile in-content slot). !important is deliberate
   here, not sloppiness: it needs to beat .ad.ad-inline's higher-specificity
   display:flex inside the 1100px media query below without touching that
   query's own logic. Delete this rule (and the grid-column pin on main
   right below, which exists only because of it) to bring ads back. */
.ad { display: none !important; }

/* .shell's three grid-template-columns tracks (168px rail / 1fr / 168px
   rail) are filled by source order via implicit auto-placement — aside.ad,
   main, aside.ad. display:none removes a grid item from placement entirely
   rather than leaving its track empty, so with both .ad rails hidden above,
   main (now the only participating item) auto-placed into track 1 — the
   168px rail column — instead of the 1fr middle one, squeezing the whole
   page into a narrow left strip. Pinning main to its track explicitly
   makes that placement correct regardless of how many siblings are hidden. */
main { grid-column: 2; min-width: 0; }

/* ── header ──────────────────────────────────────────── */
header {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 20px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--sumiInk3);
}
h1 {
  margin: 0;
  font-size: 19px;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--carpYellow);
}
h1 .sigil { width: 16px; height: 16px; vertical-align: -3px; }
.tagline { margin: 0; color: var(--fujiGray); font-size: 12.5px; }

/* ── loader (shown only once an operation exceeds ~1.5s) ── */
/* Plain setTimeout-driven display toggle — safe now that every wasm codec
   call actually runs in js/worker.js (see callWorker() in app.js), so the
   main thread this timer runs on never blocks, regardless of browser.
   Reparented into whichever .tabpanel is active (see app.js) so it
   overlays just that panel — hence .tabpanel needing position:relative. */
.loader {
  container-type: size;
  container-name: loader;
  display: none;
  position: absolute;
  inset: 0;
  z-index: 20;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 12px;
  background: color-mix(in srgb, var(--sumiInk0) 88%, transparent);
  border: 1px solid var(--sumiInk3);
  border-radius: 8px;
  text-align: center;
}
.loader.show { display: flex; }
.loader-status { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.loader svg { width: 26px; height: 26px; color: var(--crystalBlue); flex: none; animation: spin .8s linear infinite; }
.loader .msg { color: var(--fujiWhite); font-size: 13px; margin: 0; text-align: center; }
/* Short container (e.g. the image tab's panel before a preview has ever
   opened) — the vertical stack doesn't fit comfortably, so put the
   spinner inline to the left of the label instead. */
@container loader (max-height: 140px) {
  .loader-status { flex-direction: row; }
  .loader svg { width: 20px; height: 20px; }
}
.loader-progress {
  width: min(260px, 70%);
  height: 4px;
  border-radius: 2px;
  background: var(--sumiInk3);
  overflow: hidden;
}
.loader-bar {
  height: 100%;
  width: 0%;
  background: var(--crystalBlue);
  border-radius: 2px;
  transition: width .15s linear;
}
/* Label and percentage share one line — the loader is a compact overlay and
   a separate row for the percentage did not fit. Wraps rather than overflows
   on narrow screens. */
.loader-line {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: 4px 8px;
  min-width: 0;
}
.loader-pct {
  font-size: 12px;
  color: var(--fujiGray);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Visually hidden but announced: carries progress milestones for screen
   readers, since role=progressbar is not a live region. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.loader-cancel {
  margin-top: 4px;
  padding: 6px 16px;
  background: var(--sumiInk2);
  border: 1px solid var(--sumiInk3);
  border-radius: 5px;
  color: var(--oldWhite);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.loader-cancel:hover { background: var(--sumiInk3); border-color: var(--sumiInk4); color: var(--fujiWhite); }
.loader-cancel:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: 1px; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ── theme switch ────────────────────────────────────── */
.theme-switch {
  margin-left: auto;
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--sumiInk1);
  border: 1px solid var(--sumiInk3);
  border-radius: 7px;
}
.theme-switch button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 24px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 5px;
  color: var(--fujiGray);
  cursor: pointer;
  transition: background .12s, color .12s;
}
.theme-switch svg { width: 14px; height: 14px; }
.theme-switch button:hover { color: var(--fujiWhite); background: var(--sumiInk2); }
.theme-switch button[aria-pressed="true"] { background: var(--sumiInk3); color: var(--carpYellow); }
.theme-switch button:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: 1px; }

/* ── tabs ────────────────────────────────────────────── */
.tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--sumiInk3);
}
.tab {
  padding: 8px 18px;
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--fujiGray);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  transition: color .12s, border-color .12s;
}
.tab:hover { color: var(--fujiWhite); }
.tab[aria-selected="true"] { color: var(--carpYellow); border-bottom-color: var(--carpYellow); }
.tab:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: 1px; }
/* Unread completions on a tab that is not currently on screen. Lists are per
   tab, so a job finishing behind a hidden panel leaves nothing visible at all;
   this is the "there is something over here" mark. Deliberately quiet — it is
   not an alert, and #jobAnnounce already announces each completion. */
.tab-badge {
  display: inline-block;
  margin-left: 7px;
  padding: 0 5px;
  border-radius: 8px;
  background: var(--sumiInk3);
  color: var(--oldWhite);
  font-size: 10.5px;
  line-height: 16px;
  vertical-align: 1px;
}
/* Author CSS beats the UA stylesheet's [hidden]{display:none} at a specificity
   tie, so every rule that sets a display needs this back. It has already bitten
   .menu once and .job-img/.job-vid/.job-save once. */
.tab-badge[hidden] { display: none; }
/* Sits between the header tagline and the tabs on the home page only. The
   tagline is the claim; this says what the thing actually converts. */
.app-intro {
  margin: 0 0 14px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--fujiGray);
}
/* ── capability pills ────────────────────────────────── */
/* Says out loud what the widget below can do but cannot show: a whole batch at
   once, any size, nothing uploaded, no charge. Batch is the one nobody
   discovered — the <input> has been `multiple` and the staging list has taken a
   dropped folder since Phase 2, but every drop zone still read "drag & drop an
   image here", so someone with 200 photos had no reason to try more than one.
   Static markup with no ids (app/templates/_capabilities.php): page chrome, so
   no JS touches it and test/boot.test.mjs's id scrape never sees it. */
.caps {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 14px;
  padding: 0;
  list-style: none;
}
.caps li {
  padding: 3px 9px;
  background: var(--sumiInk1);
  border: 1px solid var(--sumiInk3);
  border-radius: 999px;
  color: var(--fujiGray);
  font-size: 11.5px;
  line-height: 1.5;
  /* Each pill is one short claim; wrapping one across two lines reads as two
     pills. The row itself wraps instead. */
  white-space: nowrap;
}
/* The batch claim leads, in the same green the drop zone uses for an armed
   drag: it is the capability the page is actually trying to teach, and a row
   of four identical pills teaches none of them. */
.caps li.cap-lead {
  background: color-mix(in srgb, var(--springGreen) 12%, var(--sumiInk1));
  border-color: color-mix(in srgb, var(--springGreen) 45%, var(--sumiInk3));
  color: var(--springGreen);
}

/* main > h1 carries its own 14px bottom margin (further down this file); with
   the pills directly under it that reads as two unrelated blocks rather than a
   heading and its subtitle. Tightened only where the two are actually
   adjacent, so a page without pills keeps the margin it had. Higher
   specificity than the bare `main > h1` rule, so file order does not matter. */
main > h1:has(+ .caps) { margin-bottom: 8px; }

.tabpanel { position: relative; } /* anchors the .loader overlay */

/* These two tabs have no toolbar above them; the drop zone takes that place
   and owns the card's rounded top edge, so the panel gets none of its own.
   Its border-top is then the divider between drop zone and control line —
   one card, two rows, rather than two boxes with a gap. */
#tab-image .panel { display: flex; border-bottom: none; }
#tab-video .panel { display: flex; border-bottom: none; }
/* Column, not row: unlike image/video's control line (one label, one
   dropdown, one button — a sentence read left to right), archive's is a
   label followed by a whole group of checkboxes followed by a button, which
   reads better stacked than wrapped mid-row. */
#tab-archive .panel { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; border-bottom: none; }

/* ── toolbar ─────────────────────────────────────────── */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 6px;
  padding: 8px;
  background: var(--sumiInk1);
  border: 1px solid var(--sumiInk3);
  border-radius: 8px 8px 0 0;
  border-bottom: none;
}

.tool {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 11px;
  background: var(--sumiInk2);
  border: 1px solid var(--sumiInk3);
  border-radius: 5px;
  color: var(--oldWhite);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  transition: background .12s, border-color .12s, color .12s, transform .06s;
}
.tool svg { width: 15px; height: 15px; flex: none; }
.tool:hover  { background: var(--sumiInk3); color: var(--fujiWhite); border-color: var(--sumiInk4); }
.tool:active { transform: translateY(1px); }
.tool:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: 1px; }
.tool[aria-pressed="true"] {
  background: var(--sumiInk3);
  border-color: var(--crystalBlue);
  color: var(--fujiWhite);
}

/* ── format dropdown (split-button: Format + caret + menu) ──────── */
.dropdown { position: relative; display: inline-flex; }
.dropdown .tool:first-child { border-radius: 5px 0 0 5px; }
.dropdown .caret { border-radius: 0 5px 5px 0; border-left: none; padding: 7px 8px; }
.dropdown .caret svg { width: 13px; height: 13px; transition: transform .12s; }
.dropdown .caret[aria-expanded="true"] svg { transform: rotate(180deg); }

.menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 10;
  display: flex;
  flex-direction: column;
  min-width: 168px;
  padding: 4px;
  background: var(--sumiInk1);
  border: 1px solid var(--sumiInk3);
  border-radius: 7px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .3);
}
/* author rules beat the UA stylesheet even at equal specificity, so plain
   .menu{display:flex} above silently defeats the hidden attribute without
   this — must stay after the base .menu rule */
.menu[hidden] { display: none; }
.menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  background: transparent;
  border: none;
  border-radius: 5px;
  color: var(--oldWhite);
  font: inherit;
  font-size: 12.5px;
  text-align: left;
  cursor: pointer;
}
.menu-item svg { width: 14px; height: 14px; flex: none; }
.menu-item:hover { background: var(--sumiInk2); color: var(--fujiWhite); }
.menu-item:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: -2px; }
.menu-sep { height: 1px; background: var(--sumiInk3); margin: 4px 6px; }

/* per-action icon accents — keyed off [data-act]/[data-panel] rather than
   .tool specifically, so the same rules also color menu-item icons in the
   format dropdown */
.accent                  { color: var(--c, var(--crystalBlue)); }
[data-act="b64enc"]  .accent { --c: var(--springGreen);  }
[data-act="b64dec"]  .accent { --c: var(--sakuraPink);   }
[data-act="qpenc"]   .accent { --c: var(--waveAqua);     }
[data-act="qpdec"]   .accent { --c: var(--oniViolet);    }
[data-act="autofmt"] .accent { --c: var(--carpYellow);   }
[data-act="htmlfmt"] .accent { --c: var(--surimiOrange); }
[data-act="xmlfmt"]  .accent { --c: var(--roninYellow);  }
[data-act="jsonfmt"] .accent { --c: var(--springGreen);  }
[data-act="jsfmt"]   .accent { --c: var(--sakuraPink);   }
[data-act="json2yaml"] .accent { --c: var(--waveAqua);   }
[data-act="yaml2json"] .accent { --c: var(--oniViolet);  }
[data-panel="enc"]   .accent { --c: var(--carpYellow);   }
[data-act="copy"]    .accent { --c: var(--springBlue);   }
[data-act="clear"]   .accent { --c: var(--autumnRed);    }

.sep { width: 1px; background: var(--sumiInk3); margin: 2px 4px; }
.spacer { flex: 1 1 auto; }

/* ── drop zone ───────────────────────────────────────── */
/* The top row of the *same* card as the control line below it: solid outer
   border, no bottom edge, no bottom rounding, so the panel's own border-top
   becomes the divider between the two. They have to read as one block —
   separated, the zone looked like an unrelated banner sitting above the tool.
   The dashed frame moved inside (::before) when that happened. It is the part
   that says "drop here", and it cannot be the card's own edge without the
   whole card looking provisional. It also carries the armed state: a
   border-style change stays readable without relying on colour alone. */
.dropzone {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  width: 100%;
  /* Explicit: Safari gives buttons a default horizontal margin, which would
     inset this row from the control line below and break the shared edge. */
  margin: 0;
  padding: 26px 16px;
  background: var(--sumiInk1);
  border: 1px solid var(--sumiInk3);
  border-bottom: none;
  border-radius: 8px 8px 0 0;
  color: var(--fujiGray);
  font: inherit;
  font-size: 12.5px;
  text-align: center;
  cursor: pointer;
  transition: background .12s, color .12s;
}
.dropzone::before {
  content: "";
  position: absolute;
  inset: 8px;
  border: 1px dashed var(--sumiInk4);
  border-radius: 6px;
  pointer-events: none;
  transition: border-color .12s;
}
.dropzone svg {
  width: 24px;
  height: 24px;
  margin-bottom: 4px;
  color: var(--sumiInk4);
  transition: color .12s;
}
.dropzone-title { color: var(--oldWhite); }
.dropzone-hint { font-size: 11.5px; }
/* Hover recolours the dashed frame, never the card's outer edge — that edge
   is shared with the control line below, and lighting it up on hover would
   make the whole card look focused. */
.dropzone:hover::before { border-color: var(--crystalBlue); }
.dropzone:hover svg, .dropzone:hover .dropzone-title { color: var(--crystalBlue); }
/* Inset ring: the zone is flush inside the card now, so an outset one would
   be drawn over the control line. */
.dropzone:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: -3px; }
.dropzone.over {
  background: color-mix(in srgb, var(--springGreen) 10%, var(--sumiInk1));
  color: var(--fujiWhite);
}
.dropzone.over::before { border-style: solid; border-color: var(--springGreen); }
.dropzone.over svg, .dropzone.over .dropzone-title { color: var(--springGreen); }

/* ── staging list ────────────────────────────────────── */
/* The middle row of the same card as the drop zone above and the control line
   below: no rounding of its own, and a top border that is the single divider
   from the zone. Hidden until something is staged, so an untouched tool is
   still the two rows it was before.
   No `display` on .staging itself — declaring one at class specificity would
   beat the UA stylesheet's `[hidden] { display: none }` and pin the panel open,
   the trap `.menu[hidden]` and `.job-img[hidden]` already document. The
   explicit override below makes that immune rather than merely absent. */
.staging {
  padding: 10px 14px;
  background: var(--sumiInk1);
  border-left: 1px solid var(--sumiInk3);
  border-right: 1px solid var(--sumiInk3);
  border-top: 1px solid var(--sumiInk3);
  font-size: 12.5px;
}
.staging[hidden] { display: none; }
.staging-head {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--fujiGray);
}
.staging-count { color: var(--oldWhite); }
.staging-clear {
  margin-left: auto;
  flex: none;
  padding: 2px 8px;
  background: transparent;
  border: 1px solid var(--sumiInk3);
  border-radius: 5px;
  color: var(--fujiGray);
  font: inherit;
  font-size: 11.5px;
  cursor: pointer;
}
.staging-clear[hidden] { display: none; }
.staging-clear:hover {
  color: var(--autumnRed);
  border-color: color-mix(in srgb, var(--autumnRed) 45%, var(--sumiInk3));
}
.staging-clear:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: 1px; }

/* A long batch scrolls inside the panel. Without the cap, twenty staged files
   push the control line — and with it the convert button the user is heading
   for — off the bottom of the screen. */
.staging-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 168px;
  overflow-y: auto;
}
.staged {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 3px 0;
}
.staged-name {
  color: var(--fujiWhite);
  /* min-width:0 is what makes the ellipsis work at all: a flex item defaults to
     min-width:auto, so a long unbreakable filename sets a min-content size the
     item refuses to shrink below — the text never truncates and the size and ×
     are pushed out of the card instead. */
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Only ever present on a file this browser cannot re-encode, so it is allowed
   to be the one loud thing in an otherwise plain list. Sits with the name
   rather than the size: it is a fact about the file, not about its bytes. */
.staged-note {
  flex: none;
  padding: 1px 6px;
  border: 1px solid color-mix(in srgb, var(--roninYellow) 45%, transparent);
  border-radius: 4px;
  color: var(--roninYellow);
  font-size: 11px;
  white-space: nowrap;
  cursor: help;
}
.staged-note[hidden] { display: none; }

.staged-size {
  margin-left: auto;
  flex: none;
  color: var(--fujiGray);
  font-size: 11.5px;
}
.staged-remove {
  flex: none;
  padding: 0 4px;
  background: transparent;
  border: none;
  color: var(--fujiGray);
  font: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.staged-remove:hover { color: var(--autumnRed); }
.staged-remove:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: 1px; }
.staged-more { padding: 3px 0; color: var(--fujiGray); }

/* Sits under the list, inside the same row of the card. Its own top border
   separates it from the files without implying it is one of them. */
.staging-autosave {
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid var(--sumiInk3);
  font-size: 11.5px;
}
.staging-autosave[hidden] { display: none; }

/* ── panels (image / encoding) ───────────────────────── */
.panel {
  display: none;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  background: var(--sumiInk1);
  border-left: 1px solid var(--sumiInk3);
  border-right: 1px solid var(--sumiInk3);
  border-top: 1px solid var(--sumiInk3);
  font-size: 12.5px;
  color: var(--oldWhite);
}
.panel.open { display: flex; }
.panel .label { color: var(--fujiGray); }

select, .filebtn, .go {
  font: inherit;
  font-size: 12.5px;
  padding: 6px 10px;
  background: var(--sumiInk2);
  color: var(--fujiWhite);
  border: 1px solid var(--sumiInk3);
  border-radius: 5px;
  cursor: pointer;
}
select:hover, .filebtn:hover, .go:hover { border-color: var(--sumiInk4); }
select:focus-visible, .filebtn:focus-visible, .go:focus-visible {
  outline: 2px solid var(--crystalBlue); outline-offset: 1px;
}
select:disabled, .filebtn:disabled, .go:disabled,
select:disabled:hover, .filebtn:disabled:hover, .go:disabled:hover {
  opacity: 0.45;
  cursor: not-allowed;
  color: var(--fujiGray);
  background: var(--sumiInk1);
  border-color: var(--sumiInk3);
}
.go { color: var(--springGreen); border-color: color-mix(in srgb, var(--springGreen) 35%, transparent); }
.go:hover { background: color-mix(in srgb, var(--springGreen) 10%, transparent); }
input[type=file] { display: none; }
.fname { color: var(--waveAqua); }

.check {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--fujiGray);
}
.toolbar .check { align-self: center; font-size: 12.5px; }
.check input[type=checkbox] {
  display: inline;
  accent-color: var(--crystalBlue);
}
/* Archive's four filter checkboxes read as one on/off list, not a row of
   independent controls like the rest of a .panel's inline elements — stacked
   and left-aligned so they scan top-to-bottom as a single group. Scoped to
   #panel-arch specifically rather than styling .check-group bare, since
   nothing else in the app uses it yet and a bare rule would silently apply
   to whatever picks up the class next. */
#panel-arch .check-group {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}
/* The archive tab's own copy of .staging-autosave (ui/autosave.js keeps the
   two mirrored) — a batch-behavior preference, not a fifth filter, so it's
   forced onto its own row (#panel-arch wraps as a flex row) and split from
   the filters above it by the same divider treatment .staging-autosave
   uses, rather than reading as one more thing in that list. */
#panel-arch .arch-autosave {
  flex-basis: 100%;
  padding-top: 9px;
  border-top: 1px solid var(--sumiInk3);
}

/* ── editor ──────────────────────────────────────────── */
.editor {
  position: relative;
  background: var(--sumiInk1);
  border: 1px solid var(--sumiInk3);
  border-radius: 0 0 8px 8px;
}
textarea {
  display: block;
  width: 100%;
  min-height: 380px;
  resize: vertical;
  padding: 18px 20px;
  background: transparent;
  border: none;
  color: var(--fujiWhite);
  font: inherit;
  font-size: 13.5px;
  line-height: 1.7;
  tab-size: 4;
}
textarea:focus { outline: none; }
textarea::placeholder { color: var(--sumiInk4); }
.editor:focus-within { border-color: var(--crystalBlue); }

.statusbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 7px 14px;
  border-top: 1px solid var(--sumiInk3);
  background: var(--sumiInk2);
  border-radius: 0 0 7px 7px;
  font-size: 11.5px;
  color: var(--fujiGray);
}
.statusbar b { color: var(--oldWhite); font-weight: 500; }
/* Left, not right. `margin-left: auto` pushed every status message to the far
   edge of its bar — furthest from the control that produced it. The loader
   label keeps its own centring via the more specific `.loader .msg` above,
   which also resets the margin. */
.msg { text-align: left; }
/* The text tab is the exception, because its status bar is shared: the
   char/byte/line counters own the left edge, so the message takes the right
   and the two do not run together. Every other status bar holds only a
   message. */
.editor .statusbar .msg { margin-left: auto; text-align: right; }
.msg.ok   { color: var(--springGreen); }
.msg.err  { color: var(--autumnRed); }
.msg.info { color: var(--carpYellow); }


/* ── job list ────────────────────────────────────────── */
/* One block per scheduled conversion, newest first. There is one list per tab,
   rendered *inside* its own tabpanel — which is exactly why the tab buttons
   need an unread badge: a job finishing on a tab you are not looking at is
   announced but not shown.
   (This comment described the opposite for a while: one shared list outside the
   tabpanels, and therefore no badge needed. That was the first design, replaced
   during Phase 1 because it mixed image and video results together and showed
   both under Text.) */
.job-list {
  list-style: none;
  margin: 16px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.job-list[hidden] { display: none; }
/* Clear-the-queue, sitting above the list it clears. Rendered only when 2+ jobs
   are actually waiting, so it is absent rather than disabled in the common
   case — with one queued job it would duplicate that block's own ×. It never
   touches a running job. */
.job-controls {
  display: flex;
  justify-content: flex-end;
  margin-top: 12px;
}
.job-controls[hidden] { display: none; }
.job-cancel-all {
  padding: 4px 9px;
  background: transparent;
  border: 1px solid var(--sumiInk3);
  border-radius: 5px;
  color: var(--fujiGray);
  font: inherit;
  font-size: 11.5px;
  cursor: pointer;
}
.job-cancel-all:hover {
  color: var(--autumnRed);
  border-color: color-mix(in srgb, var(--autumnRed) 45%, var(--sumiInk3));
}
.job-cancel-all:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: 1px; }

/* The finished-archive bar: one row per .zip jobs/bundle.js has ready, sitting
   above the ad and the list. Hidden until there is something to save.

   The [hidden] override is the same trap .menu and .job-ad already document: a
   bare-class `display: flex` beats the UA's `[hidden] { display: none }` on a
   specificity tie, so without this line a bar with no archives would sit on the
   page as an empty box. */
.job-bundle {
  display: block;
  margin-top: 12px;
  /* Pinned, because the bar is the one control the user needs *after* the work
     they are watching. It sits above a list that grows downwards, so before
     this a user scrolled to the newest result had the save link several
     thousand pixels above them and, in practice, never discovered the feature
     at all. Sticking it costs one row of height and removes the whole problem:
     the control is on screen at the moment it becomes usable, wherever they
     are. It sticks only for as long as its own containing block is in view,
     which is exactly the span the results occupy.

     Requires no ancestor with overflow: hidden — .tabpanel's position: relative
     does not break it, and .job-list's own overflow only engages at ~70 blocks,
     below this element rather than around it. */
  position: sticky;
  top: 0;
  /* Above .loader's z-index: 20. A per-block loader is absolutely positioned
     inside .job-stage, which has position: relative but no z-index and so opens
     no stacking context of its own — meaning a running job's loader competes
     directly with this bar and would otherwise scroll over the top of it. */
  z-index: 30;
  /* Opaque, and that is load-bearing rather than cosmetic: .bundle-part's own
     background is a translucent color-mix, so without a solid layer underneath
     the results would scroll visibly through the pinned bar. Padding gives the
     row breathing space against the viewport edge it is now flush with. */
  background: var(--sumiInk0);
  padding: 8px 0;
}
.job-bundle[hidden] { display: none; }
.job-bundle-list { list-style: none; margin: 0; padding: 0; }

/* The collecting line: on screen from the moment a bundling batch is scheduled,
   replaced in place by the archive row when the .zip is ready. No control in it
   at all — see the template for why a disabled button was the wrong answer. */
.bundle-progress {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 8px 10px;
  border: 1px dashed var(--sumiInk3);
  border-radius: 6px;
  font-size: 12.5px;
  color: var(--fujiGray);
}
.bundle-progress[hidden] { display: none; }
/* Both rows can be on screen together: a batch big enough to roll over has its
   first archive saveable while the second is still filling. */
.bundle-progress + .job-bundle-list .bundle-part:first-child { margin-top: 6px; }
.bundle-progress-count { margin-left: auto; font-variant-numeric: tabular-nums; }
.bundle-part {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid color-mix(in srgb, var(--springGreen) 30%, var(--sumiInk3));
  border-radius: 6px;
  background: color-mix(in srgb, var(--springGreen) 7%, transparent);
}
.bundle-part + .bundle-part { margin-top: 6px; }
.bundle-desc {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 12.5px;
  color: var(--fujiGray);
}
/* .dl already carries the save-link styling every result uses, so the archive
   link reads as the same action it is. Only the top margin has to go: .dl is
   written for a link sitting under a result, and this one sits in a row. */
.bundle-part .dl { margin-top: 0; flex: none; }
/* Saved rows stay — "saved" means the link was clicked, never that a file
   reached the disk, so the archive is kept until the user dismisses it. The
   fade is the only thing that marks the difference. */
.bundle-part.saved { opacity: 0.6; }
.bundle-dismiss {
  flex: none;
  padding: 0 4px;
  background: transparent;
  border: 0;
  color: var(--fujiGray);
  font: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.bundle-dismiss:hover { color: var(--autumnRed); }
.bundle-dismiss:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: 1px; }

/* One shot when an archive first becomes saveable. It fires on a row that is
   pinned and therefore already on screen, so this is a nudge rather than the
   notification itself — the bar's position is what guarantees it is seen.

   Reduced motion gets the colour change with no movement or flashing, which is
   the part of the effect that carries the meaning anyway. */
@keyframes bundle-ready {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--springGreen) 55%, transparent); }
  100% { box-shadow: 0 0 0 10px transparent; }
}
.bundle-part.just-ready {
  animation: bundle-ready 1.1s ease-out 2;
  border-color: color-mix(in srgb, var(--springGreen) 70%, var(--sumiInk3));
}
@media (prefers-reduced-motion: reduce) {
  .bundle-part.just-ready { animation: none; }
}

/* The single ad slot: a 468x60 Adcash banner directly above the first result,
   revealed by ui/ad-slot.js only once that kind's list becomes visible. It sits
   between .job-controls and .job-list in the source because that is where it
   sits on screen; see _job-list.php for why it is not an <li> inside the list.

   The [hidden] override is not optional. A bare-class `display: flex` beats the
   UA `[hidden]` rule at equal specificity — the trap `.menu[hidden]` and
   `.job-img[hidden]` are already in this file to document. This one is written
   with class+attribute specificity so it also does not depend on source order.

   Height is reserved rather than collapsed so the result below does not jump
   when the banner paints. That matters less here than it looks: this slot only
   ever appears in the moments after a convert click, and layout shift within
   500ms of a user click is excluded from CLS.

   overflow: hidden is what keeps a 468px-wide banner from giving the whole page
   a horizontal scrollbar on a narrow phone. Below 468px the banner is clipped
   rather than scaled — Adcash zones are one fixed dimension each, so the real
   fix is a second zone and a width-tiered choice, which is a later stage. */
.job-ad {
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: hidden;
  margin-top: 16px;
}
.job-ad[hidden] { display: none; }
/* Visible "Advertisement" disclosure above the banner. Deliberately styled the
   same as the .ad rails' own label — same size, tracking and muted colour — so
   an ad is marked the same way everywhere on the site rather than each slot
   inventing its own treatment.
   Given the banner's width so it aligns to the banner's left edge rather than
   floating in the middle of the card, and max-width: 100% so it is clipped
   with the banner rather than widening the row on a narrow phone. */
.job-ad-label {
  width: 468px;
  max-width: 100%;
  margin-bottom: 4px;
  color: var(--fujiGray);
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  user-select: none;
}
.job-ad-target {
  width: 468px;
  height: 60px;
  flex: none;
}
/* Retry lives in the status bar, because failed and cancelled are the only
   states that offer it and they are exactly the two with no stage to put it in.
   Pushed to the trailing edge so it never crowds the message it sits beside;
   the message flexes, so a long error shortens rather than displacing it. */
.statusbar .job-msg { flex: 1 1 auto; min-width: 0; }
.job-retry {
  margin-left: auto;
  flex: none;
  padding: 3px 9px;
  font-size: 11.5px;
}
/* A backstop, not a viewport fit. The cap used to be 78vh, which meant the list
   started scrolling inside itself at four or five results — early enough that
   scrolling a batch of conversions was the normal experience rather than the
   exception. Forty results without an internal scrollbar is the wanted
   behaviour, so the ceiling is 20x what it was; it still exists, because an
   unbounded list of a few hundred blocks would push the page copy arbitrarily
   far down, but reaching it now takes roughly seventy finished blocks rather
   than four. Retention's own 20-finished-per-medium ceiling binds long before
   this one in ordinary use, which is the intended order.
   The :has() gate stays at 4 rather than moving to 41: below the cap
   `overflow-y: auto` shows no scrollbar and changes nothing, and moving the
   gate would only trade one arbitrary number for another.
   Layout shift caused by a user click within 500ms is excluded from CLS, so the
   ranking metric is unaffected either way.
   A browser without :has() simply keeps the uncapped behaviour, which is the
   right way to degrade: no cap beats a cap that cannot be scrolled out of.
   Capping a *flex column* is what made `.job { flex: none }` above necessary —
   without it the blocks shrank to fit the cap instead of the list scrolling.
   `public/_dev-joblist-harness.html` is what checks that, since no test in this
   repo has a layout engine — and it now needs a high block count to engage the
   cap at all, which is exactly what the raised ceiling means. */
.job-list:has(.job:nth-child(4)) {
  max-height: 1560vh;
  overflow-y: auto;
  /* Focus rings on the blocks at either edge would otherwise be clipped by the
     scroll container. */
  padding: 2px 4px;
}

.job {
  background: var(--sumiInk1);
  border: 1px solid var(--sumiInk3);
  border-radius: 8px;
  overflow: hidden;
  /* Load-bearing, and not obvious. The list is a column flex container, so a
     block is a flex item and defaults to flex-shrink: 1. Normally `min-height:
     auto` would stop it shrinking below its content — but that automatic
     minimum resolves to **0** for an item whose overflow is not `visible`, and
     the `overflow: hidden` directly above (which is what clips the head and
     status bars to the rounded corners) is exactly that case.
     So the moment the :has() cap below binds at 4+ jobs, every block was
     squashed proportionally instead of the list scrolling: the preview came out
     clipped and the status bar was squeezed away entirely. The list scrolls;
     the blocks keep their own height. */
  flex: none;
}
.job-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 14px;
  border-bottom: 1px solid var(--sumiInk3);
  font-size: 12.5px;
}
.job-title {
  color: var(--oldWhite);
  /* Long filenames truncate rather than wrapping the row to three lines — which
     needs min-width:0, or the flex item refuses to shrink below its min-content
     width and pushes the dismiss × past .job's overflow:hidden edge, where it is
     invisible and unclickable rather than merely off to the right. */
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.job-dismiss {
  margin-left: auto;
  flex: none;
  padding: 0 6px;
  background: transparent;
  border: none;
  color: var(--fujiGray);
  font: inherit;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}
.job-dismiss:hover { color: var(--autumnRed); }
.job-dismiss:focus-visible { outline: 2px solid var(--crystalBlue); outline-offset: 1px; }

/* Anchors the per-job loader, which is the same .loader rules as before —
   position:absolute inset:0 — only now over this box instead of a whole
   tabpanel. min-height keeps a queued block from being a bare line, so the
   list does not jump as each job starts producing output. */
.job-stage {
  position: relative;
  padding: 14px;
}
/* No result, no stage. `done` is the only state that ever has one — a queued
   job has not started, and `cancelled`/`failed` never assign a result — so for
   every other state the preview area is just a hole between the title and the
   status line. It reappears the moment a job runs, because that is where the
   loader lives, and again on retry when the job reaches `done`.
   Only the running state reserves height, for that loader. */
.job[data-state="running"] .job-stage { min-height: 132px; }
.job[data-state="queued"] .job-stage,
.job[data-state="cancelled"] .job-stage,
.job[data-state="failed"] .job-stage { display: none; }
/* …and with no stage between them, the head's bottom border and the status
   bar's top border would stack into one 2px line. */
.job[data-state="queued"] .job-head,
.job[data-state="cancelled"] .job-head,
.job[data-state="failed"] .job-head { border-bottom: none; }

.job-img, .job-vid, .job-audio {
  max-width: 100%;
  display: block;
  border-radius: 4px;
}
.job-img, .job-vid { max-height: 320px; }
/* Load-bearing, and the same trap `.menu[hidden]` already documents: a plain
   `display: block` here beats the UA stylesheet's `[hidden] { display: none }`
   at a specificity tie, because author CSS always wins that tie. Without these
   an image job rendered an empty 300x150 <video> next to its result, and the
   save link showed on jobs that had produced nothing yet. */
.job-img[hidden], .job-vid[hidden], .job-audio[hidden], .job-save[hidden] { display: none; }
.job-img {
  background:
    conic-gradient(from 90deg at 50% 50%,
      var(--sumiInk2) 25%, var(--sumiInk3) 0 50%,
      var(--sumiInk2) 0 75%, var(--sumiInk3) 0) 0 0/16px 16px;
}
.job-vid { background: var(--sumiInk0); }
/* No max-height like .job-img/.job-vid: native audio controls have their own
   fixed height (the browser draws them, not this stylesheet), so the only
   thing worth constraining is width. */
.job-audio { width: 100%; }
.job-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 12px;
  font-size: 12px;
  color: var(--fujiGray);
}
.job-meta b { color: var(--oldWhite); font-weight: 500; }
.job-save { margin-top: 12px; }
.job[data-state="failed"] { border-color: color-mix(in srgb, var(--autumnRed) 45%, var(--sumiInk3)); }

/* ── preview ─────────────────────────────────────────── */
.preview {
  display: none;
  margin-top: 16px;
  padding: 14px;
  background: var(--sumiInk1);
  border: 1px solid var(--sumiInk3);
  border-radius: 8px;
}
.preview.open { display: block; }
.preview video {
  max-width: 100%;
  max-height: 320px;
  display: block;
  border-radius: 4px;
  background: var(--sumiInk0);
}
.preview img {
  max-width: 100%;
  max-height: 320px;
  display: block;
  border-radius: 4px;
  background:
    conic-gradient(from 90deg at 50% 50%,
      var(--sumiInk2) 25%, var(--sumiInk3) 0 50%,
      var(--sumiInk2) 0 75%, var(--sumiInk3) 0) 0 0/16px 16px;
}
.preview .meta {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 12px;
  font-size: 12px;
  color: var(--fujiGray);
}
.preview .meta b { color: var(--oldWhite); font-weight: 500; }
.dl {
  display: inline-block;
  margin-top: 12px;
  padding: 6px 12px;
  border: 1px solid color-mix(in srgb, var(--crystalBlue) 40%, transparent);
  border-radius: 5px;
  color: var(--crystalBlue);
  text-decoration: none;
  font-size: 12.5px;
}
.dl:hover { background: color-mix(in srgb, var(--crystalBlue) 10%, transparent); }

@media (max-width: 1100px) {
  .shell { grid-template-columns: minmax(0, 1fr); }
  /* .shell collapses to one track here, so main's grid-column: 2 pin above
     (correct for the 3-track wide layout) would otherwise place it in a
     phantom auto-generated column 2, leaving the one real track empty. */
  main { grid-column: 1; }
  .ad { display: none; }
  /* The sticky rails above are hidden entirely below this width, which used
     to mean zero ad inventory on mobile — most of this site's traffic,
     given the flagship query is converting iPhone photos. This in-content
     slot (see .ad-inline below) exists only at this width and reverses the
     rule above for itself; source order plus equal specificity is what
     makes this win, matching the .menu[hidden] pattern already used
     elsewhere in this file. */
  .ad.ad-inline {
    display: flex;
    position: static;
    writing-mode: horizontal-tb;
    height: 250px;
    width: 100%;
    margin: 20px 0;
  }
}

/* Below this width .panel's row-per-field layout (flex-wrap: wrap on the
   base rule above) wraps individual items, not label/control pairs — a
   label span and its own <select> can land on separate lines with nothing
   showing they belong together. Stacking the whole panel into one column
   fixes that for free: the markup already alternates label/control down
   the source (see tool.php/home.php's image and video panels), so a plain
   column keeps every label directly above the control it names without any
   wrapper element. #tab-archive .panel is untouched — it already stacks
   unconditionally (id+class beats this bare-class rule) because its "label
   + a whole group of checkboxes" shape never read well inline to begin
   with. */
@media (max-width: 640px) {
  .panel { flex-direction: column; align-items: stretch; gap: 6px; }
  /* Ties each label to the control(s) that follow it (6px, the panel's own
     gap) while opening extra space before the next label starts a new pair
     — the alternative of shrinking the base gap instead would tighten
     control-to-control spacing too, including between adjacent buttons. */
  .panel .label { margin-top: 12px; }
  .panel .go { margin-top: 12px; }
}

/* Hidden on wide viewports — the two sticky rails already cover ad
   inventory there. Height is reserved even while hidden-by-default so
   nothing shifts when the 1100px breakpoint crosses in either direction. */
.ad.ad-inline { display: none; }

/* ── page chrome shared by tool/hub/trust/directory pages (not the
   tabbed app at "/", which keeps its own header/tabs markup) ──────── */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--carpYellow);
  font-size: 15px;
  font-weight: 600;
  text-decoration: none;
}
.brand .sigil { width: 16px; height: 16px; }
.brand:hover { color: var(--fujiWhite); }

.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 10px 0 18px;
  color: var(--fujiGray);
  font-size: 12px;
}
.breadcrumb a { color: var(--fujiGray); text-decoration: none; }
.breadcrumb a:hover { color: var(--springBlue); }
.breadcrumb [aria-current="page"] { color: var(--oldWhite); }
.crumb-sep { color: var(--sumiInk4); }

main > h1 {
  margin: 4px 0 14px;
  font-size: 24px;
  color: var(--carpYellow);
}

.copy { margin-top: 28px; max-width: 68ch; }
.copy .intro { font-size: 14.5px; color: var(--oldWhite); }
.copy h2 { margin: 26px 0 8px; font-size: 16px; color: var(--fujiWhite); }
.copy h3 { margin: 16px 0 4px; font-size: 13.5px; color: var(--carpYellow); }
.copy p { margin: 0 0 10px; color: var(--oldWhite); }
.copy ul { padding-left: 20px; }
.copy .related ul, .copy .related li { list-style: none; padding: 0; margin: 0; }
.copy .related ul { display: flex; flex-direction: column; gap: 4px; }

.hub-formats { margin-top: 20px; }
.hub-formats h2 { font-size: 15px; color: var(--fujiWhite); margin-bottom: 8px; }
.hub-formats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 8px;
  list-style: none;
  padding: 0;
  margin: 0;
}
.hub-formats-grid a, .related a, .more-tools a {
  display: block;
  padding: 8px 12px;
  background: var(--sumiInk1);
  border: 1px solid var(--sumiInk3);
  border-radius: 6px;
  color: var(--springBlue);
  text-decoration: none;
  font-size: 12.5px;
}
.hub-formats-grid a:hover, .related a:hover, .more-tools a:hover { border-color: var(--crystalBlue); color: var(--fujiWhite); }

.tools-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 10px;
  list-style: none;
  padding: 0;
  margin: 18px 0;
}
.tools-grid a {
  display: block;
  padding: 10px 14px;
  background: var(--sumiInk1);
  border: 1px solid var(--sumiInk3);
  border-radius: 7px;
  color: var(--springBlue);
  text-decoration: none;
  font-size: 13px;
}
.tools-grid a:hover { border-color: var(--crystalBlue); color: var(--fujiWhite); }

.home-privacy-note {
  margin: 26px 0 0;
  max-width: 68ch;
  font-size: 13px;
  line-height: 1.55;
  color: var(--fujiGray);
}

.more-tools { margin-top: 20px; }
.more-tools a { display: inline-block; }

.site-footer {
  margin-top: 40px;
  padding-top: 16px;
  border-top: 1px solid var(--sumiInk3);
}
.footer-nav { display: flex; flex-wrap: wrap; gap: 14px; }
.footer-nav a { color: var(--fujiGray); font-size: 12px; text-decoration: none; }
.footer-nav a:hover { color: var(--springBlue); }
.footer-note { margin: 10px 0 0; color: var(--sumiInk4); font-size: 11.5px; }
.footer-credit { margin: 4px 0 0; color: var(--sumiInk4); font-size: 11.5px; }
.footer-credit a { color: var(--fujiGray); text-decoration: none; }
.footer-credit a:hover { color: var(--springBlue); }
