/* ═══════════════════════════════════════════════════════════════════════════
 *  base.css — the look of every tool on this site, in one file.
 *
 *  AI NOTES
 *  ────────
 *  EDIT THIS AND EVERY TOOL CHANGES. That is the point of it. There is a
 *  stylesheet editor at /template/ → Styles that writes this file directly.
 *
 *  THE THREE LAYERS, IN THE ORDER THEY LOAD:
 *    1. assets/chrome.css        the shared header. Not yours to restyle.
 *    2. assets/base.css          THIS FILE. Everything else, for every tool.
 *    3. <tool>/assets/app.css    a tool's own, last, so it wins — and should
 *                                therefore hold ONLY what is peculiar to it.
 *
 *  IF YOU ARE ABOUT TO RESTYLE A BUTTON IN A TOOL'S OWN SHEET, the button
 *  belongs here instead. A tool sheet that redefines a shared primitive is the
 *  beginning of two designs.
 *
 *  WHERE THIS CAME FROM: it is the generic half of what used to be the YouTube
 *  tool's private stylesheet, lifted out unchanged so that moving that tool
 *  onto the shared shell could not alter how it looks.
 *
 *  EVERY TOKEN'S VALUE IS IN :root AND NOTHING RELIES ON A var() FALLBACK. The
 *  defaults here are the design, so a fallback in a rule would be a second,
 *  quietly different value for the same thing that only ever appears when
 *  something is already broken.
 * ═══════════════════════════════════════════════════════════════════════════ */

:root {
  color-scheme: dark;

  --bg:        #071019;
  --bg-raise:  #0c1824;
  --bg-sunk:   #050c13;
  --line:      rgba(255, 255, 255, .08);
  --line-firm: rgba(255, 255, 255, .18);

  --ink:       #cfe6f2;
  --ink-bright:#eaf9ff;
  --ink-dim:   #7f9aab;
  --ink-faint: #54707f;

  --go:        #62ffb0;
  --go-soft:   rgba(98, 255, 176, .09);
  --go-line:   rgba(98, 255, 176, .32);
  --go-ink:    #a9f8cd;

  --warn:      #ffd166;
  --warn-soft: rgba(255, 209, 102, .09);
  --warn-line: rgba(255, 209, 102, .3);

  --bad:       #ff6178;
  --bad-soft:  rgba(255, 97, 120, .09);
  --bad-line:  rgba(255, 97, 120, .3);

  /* ───────────────────────────────────────────────────────────── the trace
   *
   * THE GREEN-TO-BLUE SWEEP THIS SITE SIGNS THINGS WITH. It is the light that
   * runs round a card on hover, and it is every scrollbar in every tool.
   *
   * NAMED HERE BECAUSE IT IS NOW USED TWICE. The blue end was written as a
   * literal #7cc4ff inside the card's conic-gradient and nowhere else; a
   * second copy of that literal down in the scrollbar block is two values for
   * one colour, and the day somebody adjusts the green in /template/ → Styles
   * only one of them would move. One declaration, two readers.
   *
   * --trace-a IS --go RATHER THAN A COPY OF ITS VALUE, for the same reason:
   * the accent and the start of the trace are the same green, and they should
   * not be able to disagree. */
  --trace-a:    var(--go);
  --trace-b:    #7cc4ff;
  /* The brighter pair, for hover. Stops rather than a filter — see the note
     in the scrollbar block for why nothing there can be filtered. */
  --trace-a-hi: #8dffc9;
  --trace-b-hi: #a6d9ff;

  /* THE FLAT PAIR, for the standard scrollbar properties — which take two
     colours and have no gradient in them at all. This is what Firefox gets,
     and it is a deliberate single step of the trace rather than one of its
     ends: the pure #62ffb0 is an accent meant for small marks and reads as a
     highlighter pen down the side of a whole page. */
  --scroll-thumb: #57d8a0;
  --scroll-track: #0a141d;

  /* ──────────────────────────────────────────────────────────── lift
   *
   * TWO SHADOWS PER STEP, NOT ONE. A single large blur reads as fog under the
   * card; the tight one gives it a defined edge against the page and the wide
   * one gives it the height. That pairing is what makes a panel look lifted
   * rather than smudged.
   *
   * NEGATIVE SPREAD ON THE WIDE ONE pulls it in from the sides so the shadow
   * falls mostly BELOW the card. Light comes from above, so a shadow with an
   * even halo all the way round reads as a glow instead.
   *
   * THESE ARE BLACK, NOT A DARKER GREY. On a page this dark there is very
   * little room below the background colour, and a grey shadow on a near-black
   * page is a lighter rectangle — the opposite of the effect. Opacity carries
   * the whole thing.
   *
   * NAMED RATHER THAN WRITTEN AT EACH USE for the reason --trace-a is: the
   * cards and anything that picks this up later should not be able to drift,
   * and one value adjusted in /template/ → Styles should move all of them. */
  --lift:    0 1px 2px rgba(0, 0, 0, .45), 0 8px 18px -8px rgba(0, 0, 0, .65);
  --lift-hi: 0 2px 4px rgba(0, 0, 0, .5),  0 16px 32px -10px rgba(0, 0, 0, .8);

  /* THE SAME IDEA FOR TYPE, and it is a much smaller number. Text sits ON the
     card rather than above the page, so it has almost no distance to cast
     across; anything more than a pixel or two stops reading as depth and
     starts reading as a printing fault. */
  --ink-lift:     0 1px 2px rgba(0, 0, 0, .55);
  --ink-lift-dim: 0 1px 2px rgba(0, 0, 0, .4);

  /* ───────────────────────────────────────────── TYPE ON TOP OF A PICTURE
   *
   * A DIFFERENT PROBLEM FROM THE TWO ABOVE, and it needs a different shape.
   * Those give text on a flat panel a little depth. This has to keep text
   * readable over an image nobody has seen yet — a waveform, a bright sky,
   * whatever somebody uploads — and a drop shadow alone cannot: offset down and
   * right, it leaves the top-left of every letter bare against the picture.
   *
   * SO THERE ARE TWO, AND THE SECOND HAS NO OFFSET. A 10px blur sitting
   * directly behind the glyph darkens the image in a halo all the way around
   * it, which is the same trick subtitles have always used. The first shadow
   * then does the ordinary job of lifting the letter off that halo.
   *
   * IT IS STRONGER THAN LOOKS NECESSARY ON A DARK PHOTO, on purpose. The only
   * picture that matters here is the worst one that might be uploaded. */
  --ink-on-art: 0 1px 2px rgba(0, 0, 0, .9), 0 0 10px rgba(0, 0, 0, .6);

  --r:         .55rem;
  --r-lg:      .85rem;
  --pad:       1.1rem;

  --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}

* { box-sizing: border-box; }

/* THE hidden ATTRIBUTE MUST WIN OVER EVERYTHING IN THIS FILE.
 *
 * A browser hides [hidden] with a USER-AGENT rule, and any author rule that
 * sets display beats it however unspecific it is. This file sets display on
 * .note, .row, .cards and more — so <div class="note" hidden> drew an empty
 * panel, which is exactly what Audio Edit's status banner did the moment it
 * started using these styles.
 *
 * !important IS THE RIGHT TOOL HERE AND ALMOST NOWHERE ELSE: the attribute
 * means "this is not part of the page right now", and nothing a stylesheet has
 * to say about a thing that is not there can be more true than that. */
[hidden] { display: none !important; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
}

.wrap { width: 100%; max-width: 62rem; margin: 0 auto; padding: 0 1.1rem; }
main.wrap { flex: 1; padding-top: 1.75rem; padding-bottom: 3rem; }

a { color: var(--go); text-decoration: none; }
a:hover, a:focus-visible { text-decoration: underline; }
.mono { font-family: var(--mono); font-size: .92em; }
.dim { color: var(--ink-dim); }
.faint { color: var(--ink-faint); }
h1 { font-size: 1.35rem; margin: 0 0 .2rem; color: var(--ink-bright); font-weight: 650; letter-spacing: -.01em; }
h2 { font-size: .98rem; margin: 2rem 0 .7rem; color: var(--ink-bright); font-weight: 600; }
h2:first-child { margin-top: 0; }
.lede { margin: 0 0 1.6rem; color: var(--ink-dim); font-size: .86rem; }

/* ────────────────────────────────────────────────────────────── scrollbars
 *
 * THE SAME GREEN THE CARDS TRACE ON HOVER, on every scrollbar in every tool.
 *
 * IT IS IN THIS FILE AND NOT IN A TOOL'S OWN SHEET because a scrollbar belongs
 * to the page rather than to whatever happens to be scrolling inside it. Four
 * tools each styling their own is four copies to keep in step, and the one
 * that got missed would be the one tool whose tables scroll in grey.
 *
 * ───────────────────────────────────── two mechanisms, and neither is a prefix
 * NO SINGLE PROPERTY DOES THIS EVERYWHERE, so both are declared:
 *
 *   scrollbar-color / scrollbar-width   the standard pair. Firefox, and
 *       Chrome 121+. It takes TWO FLAT COLOURS — there is no gradient in the
 *       standard at all — so it is the honest fallback: a green thumb on a
 *       sunk track, and no pretending.
 *   ::-webkit-scrollbar-*               WebKit and Blink. Takes a real
 *       background, which is where the gradient actually goes.
 *
 * A browser that understands both takes the ::-webkit- rules, because those
 * set a background and the standard property has nothing to say about one.
 *
 * ──────────────────────────────────────────────── what it does and does not do
 * THE GRADIENT RUNS WITH THE BAR — green at the start, blue at the end, down a
 * vertical thumb and across a horizontal one — so it reads as the same sweep
 * the card ring makes rather than as a stripe that is two colours by accident.
 *
 * NOTHING HERE MOVES, AND THAT IS NOT AN OVERSIGHT. The card's ring travels
 * because @property registers --card-angle as an <angle> and a keyframe turns
 * it; a scrollbar pseudo-element is not an element and neither animations nor
 * transitions reach it. The hover state therefore SWAPS THE STOPS for a
 * brighter pair rather than filtering or transitioning them — written that way
 * because the obvious version compiles, ships, and quietly does nothing.
 *
 * ON macOS THIS MAKES SCROLLBARS PERMANENT, which is worth knowing before you
 * wonder why the page got 12px narrower. The system default is overlay
 * scrollbars that fade away when you stop scrolling, and styling
 * ::-webkit-scrollbar opts a scroller into classic always-visible ones. That
 * is the trade this asks for — an invisible scrollbar cannot be green — and it
 * is why the bar is slim and the track nearly the page colour, so what is
 * always there reads as a thin accent rather than as furniture.
 *
 * THE HEADER'S MENU IS DELIBERATELY EXCLUDED, BY SPECIFICITY RATHER THAN BY A
 * RULE HERE. chrome.css hides that one on purpose — it is a 2.4rem strip and a
 * scrollbar inside it is most of its height — with .chrome-menu-qualified
 * selectors. A class beats the bare pseudo-elements below whatever the load
 * order, so nothing here reaches it, and these selectors are left bare rather
 * than given a :not() that would have to name another file's internals. */

/* scrollbar-color INHERITS, so :root reaches every scroller on the page.
   scrollbar-width DOES NOT, which is why it is on * — a container two levels
   down would otherwise keep the browser's default width while wearing the
   colour, and come out fat and green. */
:root { scrollbar-color: var(--scroll-thumb) var(--scroll-track); }
*     { scrollbar-width: thin; }

/* 12px, AND THE NUMBER IS LOAD-BEARING WITH THE 3px INSET BELOW. The two
   together leave a 6px thumb, which is about what macOS draws natively; at
   11px the visible thumb came out 5px and read as a hairline rather than as
   something you could grab. */
::-webkit-scrollbar       { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--scroll-track); }

/* THE TRANSPARENT BORDER IS THE SPACER, not a border. background-clip keeps
   the paint inside it, so the thumb floats in the track with a few pixels of
   track either side — the same way everything else on this site separates two
   marks, with a gap rather than with a line drawn around one. */
::-webkit-scrollbar-thumb {
  border-radius: 999px;
  border: 3px solid transparent;
  background-clip: padding-box;
  background-image: linear-gradient(180deg, var(--trace-a), var(--trace-b));
}
::-webkit-scrollbar-thumb:horizontal {
  background-image: linear-gradient(90deg, var(--trace-a), var(--trace-b));
}
/* The thumb also thickens by a pixel on each side, because brighter alone is
   easy to miss on a bar this slim. */
::-webkit-scrollbar-thumb:hover {
  border-width: 2px;
  background-image: linear-gradient(180deg, var(--trace-a-hi), var(--trace-b-hi));
}
::-webkit-scrollbar-thumb:horizontal:hover {
  background-image: linear-gradient(90deg, var(--trace-a-hi), var(--trace-b-hi));
}

/* WHERE A VERTICAL AND A HORIZONTAL BAR MEET. Left alone it is a pale square
   in the corner of every two-way scroller — the one piece of default chrome
   that survives styling everything around it. */
::-webkit-scrollbar-corner { background: var(--scroll-track); }

/* THE STEPPER ARROWS. Only some platforms draw them, which is exactly why they
   get hidden here: a bar that is clean on a Mac and has two grey chevrons on
   it under Windows is a difference nobody would think to look for. */
::-webkit-scrollbar-button { display: none; }

/* ------------------------------------------------------------------ header */


.pill {
  display: inline-block; padding: .18rem .55rem; border-radius: 999px;
  font-size: .66rem; letter-spacing: .04em; white-space: nowrap;
  border: 1px solid var(--line-firm); background: rgba(255, 255, 255, .04); color: var(--ink-dim);
}
.pill:hover { text-decoration: none; border-color: var(--ink-dim); }
.pill.ok   { border-color: var(--go-line);   background: var(--go-soft);   color: var(--go-ink); }
.pill.warn { border-color: var(--warn-line); background: var(--warn-soft); color: var(--warn); }
.pill.bad  { border-color: var(--bad-line);  background: var(--bad-soft);  color: var(--bad); }
.pill.off  { opacity: .75; }


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

.card {
  background: var(--bg-raise); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: var(--pad);
}
.card + .card { margin-top: 1rem; }
.card h3 { margin: 0 0 .15rem; font-size: .88rem; color: var(--ink-bright); font-weight: 600; }
.card .hint { margin: 0 0 .9rem; font-size: .76rem; color: var(--ink-dim); }

/* ------------------------------------------------------------------- forms */

label { display: block; font-size: .72rem; letter-spacing: .06em; text-transform: uppercase;
        color: var(--ink-faint); margin-bottom: .35rem; }

input[type=text], input[type=number], input[type=url], input[type=search], textarea, select {
  width: 100%; font: inherit; font-size: .88rem; color: var(--ink);
  background: var(--bg-sunk); border: 1px solid var(--line-firm);
  border-radius: var(--r); padding: .55rem .7rem;
}
/* A search field keeps its native bevel and its own font in WebKit, which makes
   it the one odd box in a row of ordinary fields. Strip the native chrome so
   the rule above is what the browser draws. */
input[type=search] { appearance: none; -webkit-appearance: none; }
input[type=search]::-webkit-search-decoration,
input[type=search]::-webkit-search-cancel-button { -webkit-appearance: none; }
textarea { resize: vertical; min-height: 5.5rem; font-family: var(--mono); font-size: .8rem; line-height: 1.5; }
input:focus-visible, textarea:focus-visible, select:focus-visible {
  outline: none; border-color: var(--go-line); box-shadow: 0 0 0 3px var(--go-soft);
}
input::placeholder, textarea::placeholder { color: var(--ink-faint); }
select { appearance: none; padding-right: 1.8rem;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-dim) 50%),
                    linear-gradient(135deg, var(--ink-dim) 50%, transparent 50%);
  background-position: calc(100% - 15px) 52%, calc(100% - 10px) 52%;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; }

.row { display: flex; gap: .7rem; flex-wrap: wrap; align-items: flex-end; }
.row > * { flex: 1 1 10rem; min-width: 0; }
.row > .tight { flex: 0 0 auto; }
.grid2 { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); }

.field { margin-bottom: .9rem; }
.field:last-child { margin-bottom: 0; }
.field .why { margin: .35rem 0 0; font-size: .73rem; color: var(--ink-faint); }

button, .btn {
  font: inherit; font-size: .82rem; font-weight: 500;
  padding: .55rem .95rem; border-radius: var(--r); cursor: pointer; white-space: nowrap;
  border: 1px solid var(--line-firm); background: rgba(255, 255, 255, .05); color: var(--ink);
}
button:hover, .btn:hover { border-color: var(--ink-dim); color: var(--ink-bright); text-decoration: none; }
button:disabled { opacity: .45; cursor: default; }
button.go, .btn.go { border-color: var(--go-line); background: var(--go-soft); color: var(--go-ink); }
button.go:hover, .btn.go:hover { border-color: var(--go); color: var(--ink-bright); }
button.danger { border-color: var(--bad-line); background: var(--bad-soft); color: var(--bad); }
button.danger:hover { border-color: var(--bad); }
button.small, .btn.small { font-size: .72rem; padding: .3rem .6rem; }
button.bare { border-color: transparent; background: none; color: var(--ink-dim); padding: .3rem .45rem; }
button.bare:hover { background: rgba(255, 255, 255, .05); color: var(--ink-bright); }

/* A pair of radio-ish buttons for the mode. The real radios stay in the DOM
   and keep working with a keyboard; only their appearance is replaced. */
.modes { display: flex; gap: .4rem; }
.modes label {
  flex: 1; margin: 0; text-transform: none; letter-spacing: 0; font-size: .82rem;
  display: flex; align-items: center; gap: .5rem; cursor: pointer;
  padding: .6rem .8rem; border-radius: var(--r);
  border: 1px solid var(--line-firm); background: var(--bg-sunk); color: var(--ink-dim);
}
.modes input { position: absolute; opacity: 0; width: 0; height: 0; }
.modes label:hover { border-color: var(--ink-dim); color: var(--ink); }
.modes label.on { border-color: var(--go-line); background: var(--go-soft); color: var(--go-ink); }
.modes label:focus-within { box-shadow: 0 0 0 3px var(--go-soft); }
.modes .ico { font-size: 1rem; line-height: 1; }
.modes .sub { display: block; font-size: .68rem; color: var(--ink-faint); }

.check { display: flex; align-items: flex-start; gap: .6rem; margin-bottom: .7rem; }
.check input { width: 1rem; height: 1rem; margin: .15rem 0 0; accent-color: var(--go); flex: 0 0 auto; }
.check label { margin: 0; text-transform: none; letter-spacing: 0; font-size: .84rem; color: var(--ink); }
.check .why { display: block; font-size: .73rem; color: var(--ink-faint); }

/* ------------------------------------------------------------------- notes */

.note {
  display: flex; gap: .8rem; align-items: center; flex-wrap: wrap;
  padding: .65rem .85rem; border-radius: var(--r); margin-bottom: 1rem;
  font-size: .82rem; border: 1px solid var(--line-firm); background: rgba(255, 255, 255, .04);
}
.note.ok   { border-color: var(--go-line);   background: var(--go-soft);   color: var(--go-ink); }
.note.warn { border-color: var(--warn-line); background: var(--warn-soft); color: var(--warn); }
.note.bad  { border-color: var(--bad-line);  background: var(--bad-soft);  color: var(--bad); }
.note span { flex: 1 1 14rem; }
.note a { color: inherit; text-decoration: underline; }

/* An inline confirmation. Replaces the browser dialogs this site does not use:
   it appears in the row it belongs to, so there is never any doubt about which
   thing is about to be deleted. */
.confirm {
  display: none; gap: .4rem; align-items: center; flex-wrap: wrap;
  font-size: .76rem; color: var(--bad);
}
.confirm.open { display: flex; }

/* ───────────────────────────────────────────────── lists of things, as cards
 *
 * USED BY THE SITE'S OWN FRONT PAGE and available to any tool that wants a grid
 * of links rather than a column of panels. a.card gets a hover because it goes
 * somewhere; the plain .card above does not, because it does not. */

.cards { display: grid; gap: .9rem; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); }

/* THE STACKING MARGIN IS FOR A COLUMN, NOT A GRID. `.card + .card` above adds a
   1rem top margin so panels stacked down a page do not touch — and it fired on
   every card in this grid after the first, which is why the second card in a
   row sat a rem lower than the first and was a rem shorter. The gap does the
   spacing here; nothing needs a margin as well. */
.cards > .card + .card { margin-top: 0; }

a.card { display: flex; flex-direction: column; text-decoration: none;
         position: relative; isolation: isolate;
         /* THE CARD SITS ABOVE THE PAGE. See --lift in :root for why it is two
            shadows and not one. Nothing moves on hover — the shadow deepens,
            which is the card answering the pointer without shifting the thing
            you are about to click out from under it. */
         box-shadow: var(--lift);
         transition: border-color .12s, background .12s, box-shadow .2s ease; }
a.card:hover { border-color: transparent; background: var(--go-soft); text-decoration: none;
               box-shadow: var(--lift-hi); }
/* FOCUS GETS THE SAME LIFT AS HOVER. A card reached by keyboard is the card
   being pointed at; giving it a flatter treatment than a mouse would get is
   the small, routine way a site ends up worse to use without a pointer. */
a.card:focus-visible { box-shadow: var(--lift-hi); }

/* ═════════════════════════════════════════ a picture behind a card
 *
 * SET IN /settings.php, per tool. The page supplies --card-bg and nothing else;
 * everything about how it is drawn is here.
 *
 * ::after RATHER THAN THE ELEMENT'S OWN background, because ::before is already
 * the hover trace and the card's background is already the panel colour that
 * shows when there is no picture. A separate layer also means the scrim and the
 * image can be one declaration — a gradient listed before an image in the same
 * background-image is painted ON TOP of it.
 *
 * z-index: -1 PUTS IT BEHIND THE TEXT BUT NOT BEHIND THE PAGE. a.card sets
 * `isolation: isolate`, which makes the card its own stacking context, so a
 * negative z-index inside it can only go as far back as the card itself. Without
 * that isolation this layer would slide behind the card's background and
 * disappear entirely, which is a genuinely baffling five minutes.
 *
 * THE SCRIM IS NOT OPTIONAL AND IT IS NOT SYMMETRICAL. The text sits top-left,
 * so the cover is heaviest there and thins towards the bottom-right where there
 * is nothing to read. A flat 50% veil over the whole card would be too dark to
 * see the picture through and still not dark enough to read white text on a
 * snowfield. Two gradients do the job a single one cannot: one across, one down.
 *
 * A PICTURE MUST NEVER COST LEGIBILITY. Whatever gets uploaded — a white
 * ceiling, a blown-out sky — the title has to stay readable, and the only way to
 * promise that is for the darkest part of the scrim to be opaque enough on its
 * own. That is what the .97 is. */
a.card.has-bg::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: var(--r-lg);
  /* ONE GRADIENT ACROSS, AND DELIBERATELY NOT ONE DOWN.
   *
   * There were two. The second ran top-to-bottom and thinned to 18% in the
   * middle of the card — which is precisely the band the two-line blurb sits
   * in, so the one piece of text that was already the dimmest on the card was
   * also the piece given the least cover. It is gone.
   *
   * ACROSS IS THE RIGHT AXIS because the text is left-aligned and fills the
   * card's full height: title at the top, blurb under it, both starting hard
   * against the left padding. Cover that column and the words are safe
   * wherever they wrap to; the picture is still the picture out to the right,
   * which is where a background has room to be seen without fighting anything.
   *
   * IT STAYS HEAVY A LONG WAY ACROSS because a blurb wraps to nearly the full
   * width — "Record a microphone, the computer's own audio, or both at once"
   * reaches the far edge. A scrim that cleared at the halfway mark would be
   * clear under the end of every second line. */
  background-image:
    linear-gradient(100deg,
      rgba(7, 16, 25, .97) 0%,
      rgba(7, 16, 25, .94) 42%,
      rgba(7, 16, 25, .84) 68%,
      rgba(7, 16, 25, .66) 100%),
    var(--card-bg);
  background-size: cover;
  background-position: center;
  /* THE CARD IS NOT EXACTLY 3:1 AND ITS RATIO MOVES WITH THE WINDOW — one
     column on a phone is much squarer than three on a desktop. cover keeps the
     picture filling the card and trims whichever axis is over, which is why the
     cropper warns about the edges rather than promising them. */
  z-index: -1;
  transition: opacity .2s ease;
  pointer-events: none;
}

/* THE PICTURE LIFTS A LITTLE UNDER THE POINTER. a.card:hover also washes
   --go-soft over the card, and that wash is BEHIND this layer — so without
   easing the scrim off, a card with a picture would be the one card on the
   page that does not respond to being pointed at.
   .88 RATHER THAN THE .82 THIS STARTED AT. Fading this layer fades the scrim
   along with the image, so every point of lift is paid for in legibility — and
   paying it on hover makes the words hardest to read at the exact moment
   somebody is leaning in to read them. */
a.card.has-bg:hover::after,
a.card.has-bg:focus-visible::after { opacity: .88; }

/* ─────────────────────────────────── THE WORDS, ONCE THERE IS ART BEHIND THEM
 *
 * THE BLURB IS THE PROBLEM, NOT THE TITLE. On a plain card --ink-dim is right:
 * it puts the description a step below the name, which is the hierarchy. Over a
 * picture that same step makes the one long piece of text on the card the one
 * hardest to read — dim grey over a waveform. On a card with art it comes up to
 * full --ink and the hierarchy is carried by size and weight instead, which
 * survive a background in a way that low contrast does not.
 *
 * SCOPED TO .has-bg so a card with no picture is untouched. */
a.card.has-bg b    { color: #fff;         text-shadow: var(--ink-on-art); }
a.card.has-bg span { color: var(--ink);   text-shadow: var(--ink-on-art); }
a.card.has-bg code { color: var(--ink-dim); text-shadow: var(--ink-on-art); }

/* ───────────────────────────────────────────── the outline that traces the card
 *
 * ON HOVER A LIGHT RUNS ROUND THE EDGE. One rotating conic gradient, clipped to
 * a one-pixel ring, so what you see is a bright arc travelling the perimeter
 * and fading back into the border behind it.
 *
 * HOW IT IS A RING AND NOT A FILLED PANEL: the pseudo-element is painted with
 * the gradient, then masked with two stacked boxes — one the size of the
 * content area, one the whole element — composited so they cancel. What is
 * left is the difference between them, which is the border. There is no other
 * way to put a gradient in a border; border-image cannot do rounded corners.
 *
 * @property IS WHAT MAKES IT MOVE. A custom property is a string to the
 * animation engine unless it has been given a type, so plain CSS cannot
 * interpolate --angle from 0 to a turn; registering it as an <angle> is what
 * turns the keyframes below from a jump into a rotation. Where @property is
 * not supported the gradient simply sits still — a static two-tone edge, which
 * is a perfectly good hover state and not a broken one.
 *
 * THE BORDER GOES TRANSPARENT ON HOVER, above, so the ring replaces it rather
 * than sitting on top of a second line of a different colour. */
@property --card-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

a.card::before {
  content: "";
  position: absolute; inset: 0;
  border-radius: var(--r-lg);
  padding: 1px;                      /* the thickness of the ring */
  /* THE SAME TWO COLOURS THE SCROLLBARS WEAR, from --trace-a/--trace-b in
     :root. They were a var() and a literal #7cc4ff written here; the literal
     is now named, so adjusting the sweep in /template/ → Styles moves the card
     ring and every scrollbar together instead of one of them. Identical
     values — this changes nothing about how the card looks. */
  background: conic-gradient(from var(--card-angle),
              transparent 0deg, transparent 50deg,
              var(--trace-a) 95deg, var(--trace-b) 130deg,
              transparent 185deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: 0;
  transition: opacity .2s ease;
  pointer-events: none;
}

a.card:hover::before,
a.card:focus-visible::before {
  opacity: 1;
  animation: card-trace 2.4s linear infinite;
}

@keyframes card-trace { to { --card-angle: 360deg; } }

/* SOMEBODY WHO HAS ASKED FOR LESS MOTION GETS THE EDGE WITHOUT THE TRAVEL:
   the ring still appears, so the card still answers the pointer, but nothing
   runs around it. */
@media (prefers-reduced-motion: reduce) {
  a.card:hover::before,
  a.card:focus-visible::before { animation: none; }
  a.card:hover { border-color: var(--go-line); }
}

/* ONE HEIGHT FOR EVERY CARD: a line of title and exactly two of blurb.
 *
 * A GRID OF PANELS THAT ARE ALL DIFFERENT HEIGHTS reads as a list of things of
 * different importance, which is not what a list of tools is. The obvious fix
 * is a fixed pixel height, and it is the wrong one: the moment somebody changes
 * a font size in /template/ → Styles the number is stale and the text either
 * rattles around or spills out.
 *
 * SO THE HEIGHT IS DERIVED FROM THE TYPE INSTEAD. The title is held to one
 * line and the blurb to two — clamped with an ellipsis when it is longer,
 * and holding the space open with min-height when it is shorter. Change the
 * font sizes and the cards simply resize together. */
a.card b {
  display: block; color: var(--ink-bright); font-size: .95rem; font-weight: 620;
  margin-bottom: .2rem; line-height: 1.4;
  /* THE TITLE CASTS THE DEEPER OF THE TWO. It is the brightest thing on the
     card, so it has the most contrast to play with; the blurb below is dimmer
     already and the same shadow on it would read as mud. */
  text-shadow: var(--ink-lift);
  /* A tool's name is short. One that is not is trimmed rather than allowed to
     wrap, because a two-line title is a card a head taller than its neighbours. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
a.card span {
  color: var(--ink-dim); font-size: .82rem; line-height: 1.5;
  text-shadow: var(--ink-lift-dim);
  /* A LINE BREAK TYPED IN SETTINGS IS A LINE BREAK HERE.
     pre-line and not pre: newlines are honoured, but runs of spaces and the
     indentation of the PHP that prints this still collapse the way they do
     everywhere else. `pre` would turn the source file's own formatting into
     part of the card.
     NOTHING IS CONVERTED TO <br>. The context is somebody's typing and goes
     through tools_h() — building HTML out of it to get line breaks would mean
     deciding which tags to allow, which is a decision this card does not need
     to have. One CSS property does the whole job. */
  white-space: pre-line;
  /* -webkit-box is still the only widely-supported way to clamp to N lines;
     line-clamp is the standard property and is ignored where unsupported. */
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
  /* 2 lines at the line-height above. em, not rem: it follows this element's
     own font-size, so the two numbers can never disagree. */
  min-height: 3em;
}
a.card code { color: var(--ink-faint); font-size: .72rem; text-shadow: var(--ink-lift-dim); }

/* A heading for a group of cards. NOT h2 — h2 is a section heading inside a
   tool's page and is set in a different key entirely. */
.group {
  margin: 2rem 0 .8rem; font-size: .72rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-faint); font-weight: 600;
}
.group:first-of-type { margin-top: 0; }

/* Nothing here yet. */
.empty {
  padding: 2rem; border: 1px dashed var(--line-firm); border-radius: var(--r-lg);
  color: var(--ink-dim); font-size: .9rem; text-align: center;
}

/* ═════════════════════════════════════════ the two ways out of settings
 *
 * DRAWN BY THE SHELL, NOT BY ANY PAGE — shell_settings_exit(), which fires for
 * any page keyed 'settings'. The long why is in shell.php; what matters here
 * is that no tool's stylesheet should ever restyle these. They are the same
 * control on every settings page on the site, which is the entire point.
 */

/* position:relative ON THE COLUMN, so the ✕ hangs off the content rather than
   the viewport. Fixed to the viewport it would float over the page while
   scrolling and sit on top of the header on a short one. */
.wrap.is-settings { position: relative; }

.set-x {
  position: absolute; top: 0; right: 0;
  width: 2rem; height: 2rem;
  display: grid; place-items: center;
  border-radius: var(--r); border: 1px solid var(--line);
  color: var(--ink-dim); text-decoration: none; font-size: .9rem; line-height: 1;
  transition: background .15s, color .15s, border-color .15s;
}
.set-x:hover { background: var(--bad-soft); border-color: var(--bad-line); color: var(--bad); text-decoration: none; }
.set-x:focus-visible { outline: none; border-color: var(--go-line); box-shadow: 0 0 0 3px var(--go-soft); }

/* THE HEADING MUST NOT RUN UNDER IT. The ✕ is taken out of flow, so without
   this a long <h1> would slide beneath the button on a narrow window. */
.wrap.is-settings > h1:first-of-type { padding-right: 2.6rem; }

/* AFTER THE LAST SETTING, where the eyes already are at the end of a read. Not
   sticky: a bar pinned to the bottom of the viewport covers the last field on
   a phone, which on a settings page is a field somebody is trying to use. */
.set-done {
  margin-top: 2rem; padding-top: 1.2rem;
  border-top: 1px solid var(--line);
  display: flex; justify-content: flex-end;
}

/* ─────────────────────────────────── a short list of facts, as a table
 *
 * SHARED RATHER THAN PER-TOOL. The Recorder grew a .rec-table for exactly this
 * — a label on the left, a value on the right — and the next settings page
 * that needs one should not grow a third. Label column stays narrow; the value
 * takes the rest.
 */
.kv { width: 100%; border-collapse: collapse; font-size: .8rem; margin: .2rem 0 1rem; }
.kv th, .kv td { text-align: left; padding: .45rem .2rem; border-bottom: 1px solid var(--line); vertical-align: top; }
.kv th { font-weight: 600; color: var(--ink-dim); width: 8.5rem; }
.kv td { color: var(--ink); }
.kv tr:last-child th, .kv tr:last-child td { border-bottom: 0; }

/* A colour, shown rather than spelled. --c is set per element by the page;
   a hex nobody can picture is not an answer to "which blue". */
.swatch {
  display: inline-block; width: 1.1rem; height: 1.1rem; margin-right: .3rem;
  border-radius: .25rem; border: 1px solid var(--line-firm);
  background: var(--c, transparent); vertical-align: -.2rem;
}

/* Small print under a control or a table: how a thing is done when the page
   cannot do it. */
.fine { margin: 1.8rem 0 0; color: var(--ink-faint); font-size: .72rem; }

code { font-family: var(--mono); font-size: .88em; color: var(--go-ink); }

/* ─────────────────────────────────────────────── naming a machine
 *
 * MOVED HERE FROM site.css, which only the site's own pages load. The front
 * page says which machine serves it; the Recorder's settings page has to say
 * which of TWO machines each half of it is talking about — the recording is
 * on the one serving the site, the transcribing is on another entirely — and
 * a tool cannot reach site.css. One chip, in the sheet every tool gets.
 *
 * THE HUE ARRIVES AS --node ON THE ELEMENT and the look is decided here. The
 * fleet gives every kind of device a colour and the page reads that table
 * rather than choosing; see app/lib/device.php.
 *
 * QUIETLY, IS THE ANSWER. A full-strength fleet colour in running text is a
 * warning light. The dot carries it at full strength because it is six pixels
 * wide; the name only borrows it. */
.node {
  color: color-mix(in srgb, var(--node) 72%, var(--ink-dim));
  font-weight: 600;
}
.node::before {
  content: ""; display: inline-block;
  width: .42rem; height: .42rem; margin-right: .36rem;
  border-radius: 50%; background: var(--node);
  vertical-align: baseline;
}
/* color-mix is recent enough to be worth a floor: without it the name simply
   takes the colour straight, which is louder than intended but never wrong. */
@supports not (color: color-mix(in srgb, red 50%, blue)) {
  .node { color: var(--node); }
}

/* A long path or filename must wrap rather than widen the page: a horizontal
   scrollbar on the body scrolls vertically too on a trackpad, which makes a
   page that jumps about while being read. */
.break { word-break: break-word; overflow-wrap: anywhere; }
