/* The archive's stylesheet.
 *
 * Lifted out of index.html, where it was 544 KB of `<style>` inside a 2.4 MB
 * file. Nothing in it changed in the move: same rules, in the same order,
 * with the same cascade.
 *
 * SERVED BY A ROUTE OF ITS OWN rather than by the static mount, so that a
 * deploy cannot leave a fresh page wearing a stale stylesheet. The reason is
 * written at that route in server.js.
 *
 * RELATIVE `url()` IN HERE RESOLVES AGAINST THIS FILE, which is served from
 * the root — so `url(icon.svg)` means `/icon.svg` wherever the page itself is
 * being served from. It did not before: the page is also served at `/m/<id>`,
 * where the same `url(icon.svg)` asked for `/m/icon.svg` and got a 404.
 */
  :root {
    /* ONE PERCENT OF THE SCREEN, IN LAYOUT PX. `zoom` scales what is drawn but
       not what `vh` MEANS: it stays 1% of the real viewport and is then scaled
       up with everything else, so a 94vh sheet on a zoomed 4K screen comes out
       1.75 screens tall. Dividing here is what makes `calc(var(--vh) * 94)`
       mean the same fraction of the screen at every step. */
    --vh: calc(1vh / var(--ui-zoom));
    --vw: calc(1vw / var(--ui-zoom));
    --bg: #141219;
    --bg-blur: rgba(20, 18, 25, 0.78);
    --surface: #211e2b;
    --surface-2: #2a2735;
    --surface-3: #332f40;
    --panel: #1b1924;
    --text: #f3f1f7;
    --text-2: #a6a1b5;
    --text-3: #6f6a7e;
    --accent: #ff4d94;          /* swap to her brand color */
    --accent-soft: rgba(255, 77, 148, 0.16);
    --on-accent: #1a0c13;
    /* The sequence mark. Pastel green ON PURPOSE and not a tint of `--accent`:
       pink is the archive's voice -- it is the hover, the focus ring, the
       "you are here" on the pool strip -- so a pink glow on a tile would read
       as a state the reader is in rather than a fact about the picture. Green
       says nothing else here, which is the whole qualification.

       Two tokens because they sit against two different things. `--pool-lit`
       is the tile's own 1px border, and the tile is dark in both rooms, so it
       does not change. `--pool-haze` is the glow, which sits on the PAGE --
       and the page is `#141219` at night and `#59525a` by day, so a pale haze
       that reads against one washes out on the other. */
    /* The third weight between "fine" and "wrong": a thing somebody should
       notice but need not hurry about. Orange ON PURPOSE and not a tint of
       `--tl-meta`, which is the yellow a clip's KIND wears — that one is a
       fact about a row and this is a state of the machine, and two meanings
       sharing a colour is how a palette stops saying anything. Used by
       `ent-notif`, and named for what it means rather than for what it is so
       the next thing that needs this weight has somewhere to go. */
    --tl-hold: #e08a3c;
    --pool-lit: #9fe3c0;
    --pool-haze: rgba(159, 227, 192, 0.36);
    /* THE PAPER A SEQUENCE IS STACKED ON. Warm and near-white because the
       thing being drawn is a pile of prints, and a print is paper whatever
       room it is in — it reads against `#141219` at night and against the
       day room's `#59525a` without either needing its own value. A token
       rather than a literal in one rule so the day room CAN take it if it
       ever wants to, which is the whole reason colours live up here. */
    --deck-card: hsl(40 11% 92%);
    /* AND THE SAME PAPER AS A SHEET RATHER THAN A SWATCH. The pile was three
       rectangles of the flat value above — a border and a fill in one tone,
       which reads as a shape somebody filled in. A print catches the light
       unevenly: the corner nearest it is near-white, the far edge has fallen a
       shade or two toward the beige the stock already is.
       SAME HUE THROUGHOUT, and that is the whole discipline here: every stop
       is hsl(40 …) like the token above, so what varies is lightness. A pile
       of paper lit from one side is one colour at several brightnesses, and
       anything else is two papers.
       THE COLOUR TOKEN STAYS as well as this one, because `.pic-sq` mixes a
       few percent of it into an empty tile's background and a gradient cannot
       go through `color-mix`. */
    --deck-paper: linear-gradient(145deg,
        hsl(40 20% 97.5%) 0%, hsl(40 13% 91%) 46%, hsl(40 12% 84%) 100%);
    /* AND THE SAME SHEET A STEP FURTHER INTO SHADOW, for the face of a deck
       that has no picture on it yet. The empty face was the ordinary tile's
       dark gradient inside a white mat, which reads as a window cut in a
       sheet rather than as a sheet — and a blank print is paper all the way
       across.
       A STEP DOWN AND NOT THE SAME VALUE, because the mat has to stay visible
       against it: identical gradients would weld the two into one shape and
       lose the edge that says there is a print inside a border. Same angle,
       same hue, lower throughout — the mat casting over the face is the only
       story these two tones have to tell between them. */
    --deck-face: linear-gradient(145deg,
        hsl(40 14% 95.5%) 0%, hsl(40 8% 87%) 52%, hsl(42 7% 79%) 100%);
    --air: #b797bd;         /* air-time arc, from the stream card */
    /* How much of the day card is ARC rather than face. The arc is a 24h clock
       — midnight at top, clockwise, length is time on air — so its angular
       POSITION is the information, and at 3px that position was the hardest
       part of it to read. The blurred `::before` copy was carrying the
       visibility and the blur carries no information. Same clamp shape as
       before so it still behaves on a narrow screen, just more of it.

       A WHOLE NUMBER OF PIXELS, and that is the whole of why no width ever
       felt right. This was `clamp(3px, 7%, 5px)`, and 7% of a cell the grid
       sizes to 68.44px is 4.78px -- so `aspect-ratio: 1` made every row's
       bottom edge land on a different fraction (88.44, 160.88, 233.31,
       305.75) and the rasteriser resolved that same inset into 4 whole pixels
       on some rows and 5 on others. One pixel on a four-pixel band is a
       quarter of it, and the fractions accumulate down the grid, so it
       alternated by ROW rather than scattering. Measured at seven viewport
       widths: the percentage varies 4-5px (a bare `7%` varies 4-6px) and any
       whole value is identical in every cell at every width.

       The cost is that it no longer scales with the cell. Container queries on
       the grid's own width would buy that back in whole-pixel steps -- keyed
       to the grid rather than the viewport, because the notes panel resizes
       the calendar without the viewport changing. Deliberately not done. */
    --cal-ring: 4px;
    /* The room switch's stroke, and the one thing on the page that has to be
       DARK in the day room and light at night — the rest of the palette moves
       the other way. --text is unchanged between rooms, so this cannot lean on
       it and gets a token of its own. */
    --room-ink: var(--text-2);
    --gutter: 40px;
    --radius: 14px;
    --radius-sm: 10px;
    --font-display: "Bricolage Grotesque", system-ui, sans-serif;
    /* The wordmark only. Separate from --font-display on purpose: swapping the
       brand face should not repaint every stream title, sheet heading and month
       label in the archive. Change this one line to try another. */
    --font-brand: Caveat, "Segoe Script", "Bradley Hand", cursive;
    --font-ui: "Plus Jakarta Sans", system-ui, sans-serif;
    --font-mono: "JetBrains Mono", ui-monospace, monospace;
  }

  /* ── the day room ──────────────────────────────────────────────────────
     Temporary, and behind a toggle on purpose: a lighter field clashes with
     the stream thumbnails as they are, and that is a separate job from this
     one. Three tokens were the ask. The other two are here because without
     them the room is not evaluable, and each says why — delete either and the
     toggle still works.

     `--bg-blur` is not a theme choice, it is the same 0.78 veil recomputed
     over the new field: left at the old value the sticky header stays a dark
     bar across a lighter page.

     `--text-3` is the secondary ink — empty-day numbers, the day-of-week
     headers, the legend, and a good deal of the rest of the site. Against
     #141219 it reads at 3.57:1; against #59525a it drops to 1.45:1 and
     effectively disappears. It takes `--text-2`'s value rather than a new
     hex — one step up the ramp that already exists, which lands at 3.02:1. */
  html[data-room="day"] {
    --bg: #59525a;
    --bg-blur: rgba(89, 82, 90, 0.78);
    --air: #ef9fc6;
    --text-3: var(--text-2);
    --room-ink: var(--surface-2);
    /* Deeper, not paler: the haze is competing with #59595a here, and the
       pastel it uses at night is almost the same value as this page. */
    --pool-haze: rgba(47, 138, 99, 0.46);
  }

  * { box-sizing: border-box; }
  /* ── SCALING THE WHOLE APP ON A BIG SCREEN ────────────────────────────────
     Every length in this file is written for a viewport of about 2,060 CSS px
     -- the 1,980px `.wrap` plus its two gutters -- and nothing in it grows. On
     a 4K screen at 100% that is a sixth of the width being used at half the
     apparent size, and the way somebody fixes it by hand is to press Ctrl-+
     until it matches. 175% on 3840 is the number that lands, which is
     3840 / 1.75 = 2194: the design width.

     SO THE PAGE DOES THAT ITSELF. `zoom` is exactly the browser's own gesture
     and it reaches EVERYTHING -- borders, radii, shadows, icon boxes, the 2.5k
     px literals in this stylesheet -- which is what makes it the right tool
     and what a px-to-rem conversion would not have given: rem reaches only
     what is written in rem.

     STEPPED, AND ON HEIGHT AS WELL AS WIDTH. Each step asks for roughly
     700 layout px of height so an ultrawide does not get a 2,000px-wide page
     600px tall. A screen under about 2,300 is left alone: that is what the
     app was drawn for.

     AND MEDIA QUERIES ARE NOT ZOOMED. They see the real viewport, which is
     what these steps need and what the `max-width` breakpoints below still
     want -- a phone is a phone. The one thing `zoom` does not scale is a
     viewport UNIT, because `1vh` keeps meaning 1% of the real screen and then
     gets scaled with everything else. Hence `--vh` and `--vw` below, which
     every such value in this file goes through. */
  :root { --ui-zoom: 1; }
  @media (min-width: 2300px) and (min-height:  800px) { :root { --ui-zoom: 1.15; } }
  @media (min-width: 2600px) and (min-height:  880px) { :root { --ui-zoom: 1.25; } }
  @media (min-width: 3000px) and (min-height: 1020px) { :root { --ui-zoom: 1.45; } }
  @media (min-width: 3400px) and (min-height: 1120px) { :root { --ui-zoom: 1.6;  } }
  @media (min-width: 3800px) and (min-height: 1225px) { :root { --ui-zoom: 1.75; } }
  @media (min-width: 5000px) and (min-height: 1610px) { :root { --ui-zoom: 2.3;  } }
  html { zoom: var(--ui-zoom); }
  html { -webkit-text-size-adjust: 100%; }
  body {
    margin: 0; color: var(--text);
    font-family: var(--font-ui); line-height: 1.5; -webkit-font-smoothing: antialiased;
    /* Dot field: five radial-gradient layers on a shared 34px lattice. Tile sizes are
       co-prime multiples of 34, so where layers coincide the alpha compounds and
       brightness varies pseudo-randomly over a very long repeat. */
    background-color: var(--bg);
    background-image:
      radial-gradient(circle at 17px 17px, rgba(255,255,255,0.26) 0 1.2px, rgba(255,255,255,0) 2px),
      radial-gradient(circle at 17px 17px, rgba(255,255,255,0.20) 0 1.2px, rgba(255,255,255,0) 2px),
      radial-gradient(circle at 17px 17px, rgba(255,255,255,0.145) 0 1.2px, rgba(255,255,255,0) 2px),
      radial-gradient(circle at 17px 17px, rgba(255,255,255,0.10) 0 1.2px, rgba(255,255,255,0) 2px),
      radial-gradient(circle at 17px 17px, rgba(255,255,255,0.06) 0 1.2px, rgba(255,255,255,0) 2px);
    background-size: 238px 170px, 170px 238px, 102px 136px, 68px 102px, 34px 34px;
    background-position: 0 0, 34px 68px, 68px 34px, 34px 0, 0 0;
  }
  /* CENTRED, and it was `margin: 0`. Below the cap there is no difference and
     that is the whole of why it went unnoticed: the app was drawn at 1920 and
     the cap is 1980. Above it the shell hugged the left edge with the rest of
     the screen as dot-field — on a 4K panel, even zoomed, that is still a
     375px margin on one side and none on the other. */
  .wrap { width: 100%; max-width: 1980px; margin: 0 auto; padding: 0 var(--gutter); }
  button { font-family: inherit; cursor: pointer; }
  :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

  /* ══ THE BUTTON BASES ═════════════════════════════════════════════════
     UP HERE, BEFORE EVERY COMPONENT, and the position is load-bearing.

     These were written beside the first thing that used them, which put
     `.btn-add` nine hundred lines BELOW `.tl-add`. Same specificity, so the
     later rule won: `.btn-add { font: inherit }` quietly reset the chip back
     to 14px body text and the "+ tag" button stood five pixels taller than
     every tag beside it. The row visibly grew when it appeared. Nothing was
     wrong with either rule; they were in the wrong order.

     A base has to be able to state a default that a member can then override,
     and with equal specificity that is decided by source order alone. So the
     bases live here and every member lives with its own component, which is
     also the only arrangement that survives the stylesheet being split into
     its own file — see B2. */
  /* ══ OPERATOR ═════════════════════════════════════════════════════
     A word that does something, rather than a button that looks like one.
     Mono, upper, tracked out, no chrome at all until you are on it.

     IT IS A BASE, NOT A MODIFIER, and the three places it is going prove why.
     Every one of them was a `cs-btn` or a `.tl` CANCELLING its own base:
     `.au-del` sets `background: none; border-color: transparent`, the history
     bar sets `border-color: transparent` again, and `.tl-clear` needed
     `border: ... !important` to get out from under the chip it sat beside. A
     modifier that spends its declarations undoing the thing it modifies has
     the wrong parent; that is what `!important` in a stylesheet usually means.

     The typography is not new either. `.au-del, .au-ref, .au-add` already
     declare mono/upper/0.06em/0.66rem, and `.hx-bar .hx-clear` declares the
     same four again a thousand lines away. This is those two spellings given
     one name.

     :active IS A TRANSFORM, which is the answer to "a grow would push its
     neighbours about": transforms are composited, not laid out, so a scaled
     button cannot move anything next to it, reflow a row or change a single
     measurement. A press-in rather than a grow, because the gesture is
     pressing. */
  /* BOTH, and not by accident. `text-transform` is here so the CLASS
     guarantees the face — a new operator cannot be added in sentence case by
     forgetting. The labels are ALSO written upper in the markup, which is a
     decision `hierarchy.mjs` already recorded and gave a reason for: the word
     IS the label, and somebody copying it out of the page should get what
     they can see. The two agree, so neither is doing anything on its own. */
  .btn-op { font-family: var(--font-mono); text-transform: uppercase;
    letter-spacing: 0.06em; font-size: 0.66rem; font-weight: 500;
    background: none; border: 0; border-radius: 6px; padding: 4px 8px;
    color: var(--text-3); cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease, transform 0.07s ease; }
  /* ══ ADD ══════════════════════════════════════════════════════════
     One more of a set that already exists: another tag, another chapter,
     another grant. The dashed line is the whole idea — an outline where a
     thing is not yet.

     SIX SPELLINGS OF ONE LINE is what was here instead. `var(--surface-3)`,
     `var(--border)`, `var(--glass-line)`, and three different opacities of
     white written out by hand (0.16, 0.18, 0.2). Nobody chose six; each was
     typed once beside the thing it was for. `--dash-line` is 0.18, the middle
     of the three, and the two tokens it replaces resolve within a hair of it.

     AND THREE OF THEM WERE CANCELLING A BASE, the same disease `.btn-op`
     had: `.tl-add` carried `border: ... !important` purely to escape the
     `.tl` chip whose geometry it wanted, and `.mu-tagadd` sat on `.mu-tag`
     for the same reason. They state their own geometry now and wear this
     instead of fighting something else.

     Hover is the accent, which five of the six already did. */
  /* MONO ON THE BASE, not on three members separately. `tl-add`,
     `th-draw-btn` and `mu-tagadd` each declared it; `hx-add` and the squares
     did not and fell back to the UA's button face, which is why a row could
     hold two of these in two different fonts.
     NOT `font: inherit`, which is what stood here: that shorthand also resets
     font-size, and since every member sets its own size it only ever took one
     away. The base says the face; the member says how big. */
  .btn-add { border: 1px dashed var(--dash-line); background: none;
    color: var(--text-3); cursor: pointer; font-family: var(--font-mono);
    transition: border-color 0.12s ease, color 0.12s ease, transform 0.07s ease; }
  .btn-add:hover { border-color: var(--accent); color: var(--accent); }
  /* A DRAWN CROSS, not a typeset one. A `+` character sits on the text
     baseline inside an em box with room above it for ascenders, so centring
     the GLYPH BOX leaves the mark itself high — `place-items: center` is
     doing its job and the result is still visibly off. Every nudge that fixes
     it is specific to one font at one size. This is two strokes with no
     baseline to sit on, so it is centred because it is symmetrical. */
  .btn-add .ico-plus { display: block; width: 1em; height: 1em; }
  .btn-add:active { transform: scale(0.96); }
  @media (prefers-reduced-motion: reduce) {
    .btn-add { transition: border-color 0.12s ease, color 0.12s ease; }
    .btn-add:active { transform: none; border-style: solid; }
  }

  /* A FIXED SIZE, carried by the icon rather than by the button. The base is
     set for a WORD at 0.66rem, so an operator holding a mark instead of a
     label used to buy its size with a `font-size` bump \u2014 which is a
     typographic control doing a geometric job, and every such button bought
     its own number. 15px, once, here — the size at which the densest mark
     in the set (five strokes, `list-restart`) still resolves, measured rather
     than picked.
     `.btn-add .ico-plus` is `1em` and NOT this, on purpose: that family runs
     from a 22px square to a whole card, so its icon has to scale with the
     member. This one's members are all about the size of a word. */
  .btn-op .ico { display: block; width: 15px; height: 15px; }
  /* AND THE SAME SIZE FOR A TYPESET MARK, where the glyph is staying. `✕`
     is one character on a text baseline, so only `font-size` can reach it;
     stated once here rather than per button, and `line-height: 1` is what
     stops the em box from making the row taller than the icon ones. */
  .btn-op.op-mark { font-size: 0.95rem; line-height: 1; padding: 3px 7px; }
  .btn-op:hover { background: var(--surface-2); color: var(--text); }
  .btn-op:active { transform: scale(0.96); }
  /* THE ONE MODIFIER SO FAR, and it only ever ADDS — a colour and an
     underline on top of a base that already has no chrome to undo. That is
     the whole difference between this and the `.au-del` it replaces, which
     had to spend two declarations cancelling `cs-btn` before it could say
     anything of its own.
     Applied in the MARKUP rather than overridden back out in CSS: both call
     sites already branch on state (`Keep` vs `Delete`, `Keep` vs `Rebuild`),
     so the class rides the same condition the word does. That deleted two
     rules whose entire job was to put the colour back. */
  .btn-op.op-danger { color: var(--tl-not); }
  .btn-op.op-danger:hover { color: var(--tl-not); background: none;
    text-decoration: underline; }
  /* The one thing a composited transform cannot do for somebody who has
     turned motion off. The colour change is the whole feedback there. */
  @media (prefers-reduced-motion: reduce) {
    .btn-op { transition: background 0.12s ease, color 0.12s ease; }
    .btn-op:active { transform: none; background: var(--surface-3); }
  }

  /* ══ ENTRY ═════════════════════════════════════════════════
     An operation on ONE THING THE ARCHIVE HOLDS — a stream, a snippet, a
     picture, a song. Four surfaces, the same handful of verbs on each (copy,
     download, approve, unlist, replace, delete, history), and until now four
     drawings of them.

     WHAT IT COST TO HAVE FOUR. `card-tool` was a 22px square with a 12px icon
     and no border; `pic-b` a 25px square with a 13px icon and a faint one;
     `mu-btn` a 30px square with a 15px icon and a real one. Three boxes,
     three icon sizes, and three different things hover meant: a fill ramp, an
     accent, a text colour. The same `Delete` button looked like three
     buttons depending on which panel you were in.

     `mu-btn` IS THE TEMPLATE, by decision — it was the only one of the three
     with a border at rest, which is what makes a lone square read as a
     control rather than as decoration sitting on somebody's artwork.

     THE ICON SIZE IS HERE and the call sites no longer carry `width`/`height`
     attributes, for the reason `.btn-op .ico` gives: a presentation attribute
     is a per-button number, and per-button numbers are how 13 pixel sizes and
     11 stroke widths got into this file. */
  .btn-ent { width: 30px; height: 30px; display: grid; place-items: center;
    padding: 0; border-radius: 8px; cursor: pointer; font: inherit;
    border: 1px solid var(--border); background: transparent;
    color: var(--text-3);
    transition: color 0.14s ease, border-color 0.14s ease, background 0.14s ease,
                transform 0.11s ease; }
  .btn-ent svg { display: block; width: 15px; height: 15px; }
  .btn-ent:hover { color: var(--text); border-color: var(--text-3); }
  /* THE AFFIRMATIVE. Filled, because approving is the one verb in the set
     that is meant to be found without reading the tooltips.
     AND IT STAYS FILLED. It was briefly outlined-in-accent, on a misreading
     of a request that was about `#rv-approve-all` in the review toolbar —
     see `.rv-go`. The two cases look identical and are opposite: here every
     sibling is an outlined square of the same size, so filling this one is
     how it is found; there every sibling is a glass pane, and the odd one out
     was odd by being a different material rather than a louder one. */
  .btn-ent.ent-yes { background: var(--accent); color: var(--on-accent);
    border-color: transparent; }
  .btn-ent.ent-yes:hover { filter: brightness(1.08); color: var(--on-accent); }
  /* THE RED ONE, which was `bad` on the music card and `danger` on the other
     two — two names, one tint, and neither said what it meant. It is a
     WARNING and not a danger: almost everything wearing it is reversible —
     unlist relists, a retraction restores, a swap waits for an editor, a
     stream delete tombstones behind an Undo. The colour asks for a second's
     attention; it does not claim the thing is about to be gone.
     THE EXCEPTION, AND IT IS UNSETTLED: `purge` ("Delete the file for good")
     wears the same mark and is the one verb in the set that is not coming
     back. One tint was the instruction and one tint is what this is; whether
     the irreversible one needs a louder mark than the reversible seven is a
     decision nobody has made yet, not a thing that was decided this way.
     At rest it is the ordinary grey. A row of permanently red buttons stops
     being read after a day, and these sit four and five to a card. */
  .btn-ent.ent-warn:hover { color: var(--tl-not);
    border-color: color-mix(in srgb, var(--tl-not) 55%, transparent); }
  /* `--tl-not` and not `var(--bad, #e5484d)`, which is what the music card
     used: the fallback form means the token may not exist, and this is the
     red the archive already wears for a thing taken out — the excluded
     filter chip, `op-danger`, the removal marks in the ledger. One red. */
  /* THE ORANGE ONE, and it is a third weight rather than a second red. Where
     `ent-warn` means "this takes something out of the archive, reversibly",
     this means "this has a consequence that accumulates": a paused queue
     keeps accepting work and does none of it. Nothing is lost and nothing is
     at risk — but the longer it goes unnoticed the more there is to catch up
     on, which is a different kind of attention to ask for.
     AND IT LATCHES, which no other member of this family does. `is-paused`
     keeps the tint on at rest, because the state it reports outlives the
     gesture that set it: a pause button that looked ordinary while paused
     would be a control whose own appearance denied what it had just done. */
  /* AND IT GIVES UNDER THE PRESS. The one button in this family whose press
     changes a state rather than naming a row, so it is the one that wants to
     feel like it did something — and the feedback is bound to `:active`, which
     means it cannot be re-triggered by the poll that redraws this card every
     few seconds. A keyframe hung off `.is-paused` would have fired again on
     every repaint while the queue stayed paused. */
  .btn-ent.ent-notif:active { transform: scale(0.88); }
  /* AND IT IS THE ONE MEMBER OF THIS FAMILY WITH NO BOX. A border says "this
     is a control", which is worth saying for a bare glyph in a row of text and
     is not worth saying here: this one sits between a picker and a switch,
     both of which already say it, and the three outlines read as a toolbar
     rather than as three different kinds of thing.
     THE LATCH SURVIVES IT, which is the only thing the border was load-bearing
     for. `is-paused` keeps the tint and the soft fill at rest, and a lit shape
     with no edge is still unmistakably lit — see the note above on why the
     paused state has to outlive the gesture. */
  .btn-ent.ent-notif { border-color: transparent; }
  .btn-ent.ent-notif:hover,
  .btn-ent.ent-notif.is-paused { color: var(--tl-hold);
    background: color-mix(in srgb, var(--tl-hold) 12%, transparent); }
  .btn-ent.copied { color: var(--accent);
    border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
  /* A COPY THAT SAYS NOTHING looks like a button that did nothing, so the
     icon stands aside for a word while the state lasts. Lifted off
     `.sheet-ico`, which is where the trick was written: it belongs to the
     state and not to one surface, and the music card's copy button wanted it
     too. Harmless on the members with no `<b>` to show. */
  .btn-ent b { display: none; font-family: var(--font-mono); font-size: 0.5rem;
    font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
  .btn-ent.copied b { display: block; }
  .btn-ent.copied svg { display: none; }

  /* ---- Top bar ---- */
  .topbar { position: sticky; top: 0; z-index: 10; background: var(--bg-blur); backdrop-filter: saturate(140%) blur(12px); }
  .topbar-inner { display: flex; align-items: center; gap: 14px; height: var(--topbar-h); }
  .brand { display: flex; align-items: baseline; gap: 10px; }
  /* The icon is masked, not <img>-ed. Its fills and strokes are baked as
     #231f20, and CSS cannot reach inside an external image to recolour it — a
     mask uses only the shape's alpha, so `background` decides the colour and it
     stays var(--accent) like the diamond before it. Inlining the SVG would also
     work but drops its <style> block's .cls-1…5 and its Layer_* ids into the
     page's global namespace, which is a collision waiting to happen for no gain.

     26px, not the diamond's 11: the artwork is line art with 0.75px and 2px
     strokes over a 39-unit viewBox, so at 11px those render at 0.21px and
     0.56px — sub-pixel, and the face collapses into a smudge. It starts reading
     as a fox around 20px and is comfortable at 26. The topbar is 64px, so there
     is room. Width is height x 0.879, the viewBox's own ratio. */
  .brand-mark { width: 23px; height: 26px; flex: none; align-self: center;
    background: var(--accent);
    -webkit-mask: url(icon.svg) no-repeat center / contain;
    mask: url(icon.svg) no-repeat center / contain; }
  /* A button, not a span: it resets the index, and something that changes the
     page on click has to be reachable from the keyboard. All the button
     chrome is stripped back off. */
  /* 1.75rem where Dosis sat at 1.3: a script face carries far less ink per em
     and the wordmark read as a footnote at the old size. The login page's own
     wordmark is 2.15rem; this one shares a 64px topbar with the platform
     icons, so it stops short of that. */
  .brand-name { font-family: var(--font-brand); font-weight: 700; font-size: 1.75rem;
    letter-spacing: 0.005em; padding: 2px 4px; margin-left: -4px; border: 0;
    background: transparent; color: inherit; cursor: pointer; border-radius: 7px;
    transition: color 0.15s ease; }
  .brand-name:hover { color: var(--accent); }
  .brand-name:hover span { color: var(--accent); opacity: 0.75; }
  .brand-name span { color: var(--text-3); transition: color 0.15s ease, opacity 0.15s ease; }
  .brand-links { display: flex; align-items: center; gap: 6px; margin-left: 8px; align-self: center; }
  /* Both icons rest uncoloured. --live is per-platform so one .is-live rule
     paints YouTube red and Twitch purple without the markup knowing which is
     which — paintLive() only ever toggles the class. Resting is also what
     "we have not been told" looks like, which is the honest default while
     nothing feeds it. */
  .plat { position: relative; width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center;
          color: var(--text-3); background: var(--surface); transition: color 0.15s ease, background 0.15s ease; }
  .plat:hover { background: var(--surface-2); }
  .plat--yt { --live: #ff4d4d; --live-soft: rgba(255, 77, 77, 0.13); }
  .plat--tw { --live: #a970ff; --live-soft: rgba(169, 112, 255, 0.14); }
  .plat--yt:hover { color: #ff4444; }
  .plat--tw:hover { color: #a970ff; }
  .plat.is-live { color: var(--live); background: var(--live-soft); }
  .live-tag { display: none; position: absolute; top: -7px; left: 50%; transform: translateX(-50%);
              font-family: var(--font-mono); font-size: 0.5rem; font-weight: 500; letter-spacing: 0.09em;
              color: #fff; background: var(--live); padding: 1.5px 4px; border-radius: 3px; line-height: 1;
              box-shadow: 0 0 0 2px var(--bg); }
  .plat.is-live .live-tag { display: block; }
  .plat.is-live::after { content: ''; position: absolute; inset: 0; border-radius: 9px;
                         box-shadow: 0 0 0 1px var(--live); opacity: 0.45;
                         animation: livePulse 2.2s ease-in-out infinite; }
  @keyframes livePulse { 0%, 100% { opacity: 0.2; } 50% { opacity: 0.6; } }
  /* nowrap and a hide breakpoint of its own: this sits in a flex row inside a
     topbar with a hard 64px height and no overflow rule, so a string long
     enough to be squeezed wraps to two lines and pushes the sticky header
     taller — which .cal-notes { top: 84px } is manually offset against. The
     560px rule further down is far too late for a string this long.
     Still true with the pill gone: it was chrome, not a layout constraint. */
  .brand-eyebrow { margin-left: 6px; font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); white-space: nowrap; }
  @media (max-width: 1080px) { .brand-eyebrow { display: none; } }

  /* ---- contact, and who does the pushing --------------------------------
     `.who` carried `margin-left: auto` and therefore owned the whole gap
     between the wordmark and the right end. TWO AUTO MARGINS IN ONE FLEX ROW
     SPLIT THE FREE SPACE between them, which would have parked this block in
     the middle of the bar — so it takes the push and `.who` gives it up. */
  .contact { margin-left: auto; display: flex; align-items: center; gap: 8px; }
  .topbar-inner:has(.contact) .who { margin-left: 0; }
  /* The eyebrow's typography, because it says the same kind of thing: mono
     small caps for the words about the archive rather than in it. */
  /* NOTHING UNTIL IT IS POINTED AT. Both of these sit in a row of chrome that
     is already busy, and a resting border on each would read as two more
     controls competing with the wordmark. `--glass-hi` is the white-with-
     transparency this sheet already hovers with — `.ops-ac button`,
     `.rec-sync button` and `.moment-bar button` all use it — so it holds up
     in the day room as well as the night one.
     THE BORDER IS TRANSPARENT RATHER THAN ABSENT, so the box does not change
     size between the two states. */
  .contact-btn { flex: none; padding: 5px 10px; border-radius: 9px;
    border: 1px solid transparent; background: none; color: var(--text-3);
    cursor: pointer; font-family: var(--font-mono); font-size: 0.66rem;
    letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; }
  .contact-btn:hover, .contact-btn[aria-expanded="true"] { color: var(--text);
    background: var(--glass-hi); }
  /* Later than `.plat`'s own two rules, which is what lets it drop the
     resting surface its siblings keep. */
  .plat--kofi { background: none; }
  .plat--kofi:hover { color: #ff5e5b; background: var(--glass-hi); }
  .ct-ico { display: none; }
  /* IT SHORTENS, IT DOES NOT GO. The eyebrow beside it hides outright, which
     is right for a disclaimer and wrong for a control — a phone is where most
     of the people who need this are, and a bar with no way to reach anybody
     is the one thing this block exists to prevent. So: "Contact us", then
     "Contact", then the mark alone at the width where the row runs out.
     MEASURED, NOT GUESSED. At 390px the bar came to 395 against 390 of room
     before this landed, so there was no slack to spend: the word costs about
     80px and that is most of what the mark gives back. */
  @media (max-width: 720px) { .ct-us { display: none; } }
  @media (max-width: 560px) {
    .ct-word { display: none; }
    .ct-ico { display: block; }
    .contact-btn { width: 30px; height: 30px; padding: 0; border-radius: 9px;
      display: grid; place-items: center; }
    /* AND THE ROW HAS TO GIVE SOMETHING BACK, because at 390px it came to 395
       against 390 of room BEFORE any of this: there was no slack to spend and
       the mark instead of the word only returns 80 of the 82 this costs.
       THE CHANNEL LINKS ARE WHAT GO, 66px of them. The archive is about those
       two channels and every entry in it links out to them, so they have a
       second home on every page; the contact button and the account menu have
       none. The gap closing from 14 to 8 is the last 24. */
    .brand-links { display: none; }
    .topbar-inner { gap: 8px; }
    /* It was off the right edge before any of this, so this is the first
       width at which anybody has seen it wrap. */
    .who [data-who="in"] { white-space: nowrap; }
  }

  /* THE ROOM SWITCH. In the topbar rather than in .cal-nav because the room is
     the whole page's and not the calendar's — and unlike the eyebrow it shares
     a row with, it does NOT hide at 1080px: a disclaimer you cannot read on a
     laptop is a nuisance, a control you cannot reach is a bug. */
  .room-btn { flex: none; display: inline-grid; place-items: center;
    width: 30px; height: 30px; margin-left: 8px; padding: 0; border: 0;
    background: transparent; border-radius: 9px; cursor: pointer;
    color: var(--room-ink);
    transition: color 300ms ease, background 150ms ease; }
  .room-btn:hover { background: var(--surface); }
  /* The spin is on the icon, not the button: the button has a hover chip and
     rotating that would wobble a square through 45deg on the way round. */
  .room-btn svg { display: block; width: 18px; height: 18px;
    transition: transform 620ms cubic-bezier(0.22, 1, 0.36, 1); }

  /* ---- Tab switcher ---- */
  /* STICKY UNDER THE SITE BAR, which it was not. The upload drawer docks below
     both of these (see `--up-top`), and a bar that scrolls away would leave a
     49px strip of the page showing above a `position: fixed` panel. It is
     already built for this — opaque `--surface`, a hairline along its bottom —
     and the tabs being reachable from halfway down a long gallery is worth
     having on its own. `z-index: 9` is the toolbar's, one under `.topbar`. */
  .tabbar { position: sticky; top: var(--topbar-h); z-index: 9;
    background: var(--surface); box-shadow: inset 0 -1px 0 rgba(255,255,255,0.05); }
  .tabs { position: relative; display: flex; gap: 2px; padding-top: 6px; padding-bottom: 6px; }
  .tab { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 8px; border: 0; background: transparent; color: var(--text-2); font-weight: 600; font-size: 0.94rem; padding: 9px 16px; border-radius: 9px; white-space: nowrap; transition: color 0.2s ease; }
  /* Sized here rather than on the elements, so they are all the same however
     their source was drawn. Dimmer than the label until the tab is the one you
     are on or reaching for: an icon at full strength beside a muted word reads
     as the icon being the thing, and the word is the thing. */
  .tab svg { width: 16px; height: 16px; flex: 0 0 auto; opacity: 0.7;
    transition: opacity 0.2s ease; }
  .tab:hover svg, .tab[aria-selected="true"] svg { opacity: 1; }
  .tab:hover { color: var(--text); }
  .tab[aria-selected="true"] { color: var(--text); }
  /* Pushed to the far end of the bar. `margin-left: auto` inside the flex row
     rather than a second container, so the indicator — which is positioned
     from each tab's own offsetLeft — follows it there without knowing why. */
  .tab.tab-admin { margin-left: auto; }
  .tab-indicator { position: absolute; top: 0; left: 0; z-index: 0; background: var(--surface-3); border-radius: 9px; transition: left 0.28s cubic-bezier(0.4,0,0.1,1), width 0.28s cubic-bezier(0.4,0,0.1,1); }


  /* ---- Admin ---------------------------------------------------------------
     Two lists that answer the same question from opposite ends: which taglets
     gate, and who is let through. Side by side because that is how the pair is
     actually read — you come here holding one and looking for the other. */
  /* ops-, and NOT ad-, which is what these were called for a long time.
     Every one of them was invisible to anybody running an ad blocker: the
     generic cosmetic filters in EasyList and friends hide elements whose class
     begins `ad-` on sight, so `.ad-grid` was display:none in the browser with
     nothing in this file saying so and nothing in the console to find. The
     whole admin panel rendered as its intro paragraph and a screenful of
     background. Do not name a class after an advertisement. */
  /* Two columns that stay two columns: gates on the left, roles on the right,
     and each column stacks its own pair. auto-fit would reflow them into one
     row of four the moment the window is wide enough, which puts Role
     ownership beside Gates and breaks the one thing the layout says. */
  .ops-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: 16px; align-items: start; }
  .ops-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
  .ops-card { padding: 16px 18px 18px; border-radius: 14px;
    border: 1px solid var(--border); background: var(--surface); }
  .ops-card h3 { margin: 0 0 4px; font-size: 0.95rem; }
  .ops-note { margin: 0 0 14px; color: var(--text-3); font-size: 0.76rem; line-height: 1.5; }

  /* ---- a card that opens ---------------------------------------------------
     The marker is drawn here rather than left to the browser: the default
     triangle sits outside the flow and is a different size and colour on every
     platform, and this one has to line up with an h3. */
  details.ops-card { padding-bottom: 16px; }
  details.ops-card > summary { display: flex; align-items: baseline; gap: 8px;
    cursor: pointer; list-style: none; margin: -2px 0 0; }
  details.ops-card > summary::-webkit-details-marker { display: none; }
  details.ops-card > summary::before { content: '▸'; color: var(--text-3);
    font-size: 0.7rem; line-height: 1.5; transition: transform 0.12s ease;
    transform-origin: 45% 55%; }
  details.ops-card[open] > summary::before { transform: rotate(90deg); }
  details.ops-card > summary h3 { margin: 0; }
  details.ops-card > summary:hover h3 { color: var(--accent); }
  /* The reason a shut card is still worth having: the number is the answer to
     most of the questions anybody opens one of these to ask. */
  .ops-n { margin-left: auto; color: var(--text-3);
    font-family: var(--font-mono); font-size: 0.7rem; }
  details.ops-card[open] > summary { margin-bottom: 10px; }
  details.ops-card:not([open]) > summary { margin-bottom: 0; }
  /* A room with nothing in it, saying what it is for. Same register as the
     empty panels — a promise, not an invented table. */
  .ops-soon { padding: 12px 13px; border-radius: 10px; background: var(--glass-sunk);
    color: var(--text-3); font-size: 0.78rem; line-height: 1.55; }
  .ops-soon b { display: block; margin-bottom: 4px; color: var(--text-2); }
  .ops-soon code { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-2); }

  /* ---- the two queues, side by side --------------------------------------
     One grid rather than two floats, so they are the same height and the
     shorter one does not leave a hole the log climbs into. Each body scrolls
     at a height that shows about eight rows: enough to read the shape of a
     queue, bounded so a hundred jobs cannot push the log off the screen. */
  /* `.ops-queues` is gone with the pairing it existed for: the Models card
     and the Model queue were the two cards in that grid, and they are one card
     now. The worker queue was never in it. */
  .ops-scroll { min-height: 170px; max-height: 340px; overflow-y: auto;
    overscroll-behavior: contain; }
  /* The state line is written INTO the scroller by the loader, so without this
     it scrolls away and the card loses the one part of itself that is not a
     row. Only the worker queue has one — whisper's went when the Models card
     took its queue in, and the three lists in there belong to rows that are
     already labelled. */
  .ops-scroll .tq-head { position: sticky; top: 0; z-index: 1; margin: 0 0 10px;
    padding: 2px 0 8px; background: var(--surface); }
  .ops-rows { display: flex; flex-direction: column; gap: 8px; }
  .ops-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
    padding: 9px 11px; border-radius: 10px; background: var(--glass-sunk); }
  /* Namespaced, both of them. `.who` already exists in the topbar carrying
     `margin-left: auto`, and an unscoped name inheriting an auto margin is
     how every row here ended up starting a third of the way across the
     card. `.sub` is scoped everywhere else in this file for the same
     reason; it is too generic a word to leave loose. */
  .ops-who { font-weight: 600; font-size: 0.86rem; }
  .ops-sub { color: var(--text-3); font-family: var(--font-mono); font-size: 0.66rem; }
  .ops-empty { padding: 14px 2px; color: var(--text-3); font-size: 0.8rem; }
  /* The same mono small-caps `.rec-sec span` uses, because it sits under one
     and a second treatment would read as a different kind of thing. */
  /* NOT `display: flex`, which is what this wanted for the gap and cannot
     have: a `<summary>` laid out as anything but `list-item` loses the
     browser's disclosure triangle, and a shut fold with no triangle is a
     label. The count gets its gap from a margin instead, and the marker is
     the same one `.pic-tr` shows — one gesture, drawn one way. */
  .ops-fold > summary { cursor: pointer; padding: 3px 0;
    font-family: var(--font-mono); font-size: 0.63rem; font-weight: 500;
    letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-3); }
  .ops-fold > summary:hover { color: var(--text-2); }
  .ops-fold > summary .ops-n { margin-left: 8px; }
  .ops-fold[open] > summary { margin-bottom: 4px; }
  /* ---- the credit balance on a roster row --------------------------------
     Mono and right-aligned, because it is a number read in a column of
     numbers, and narrow because four digits is the most it ever holds. Flat
     until the pointer is on it: the same restraint the audit panel's record
     fields take, so a row reads as a fact with one correctable number in it
     rather than as a form. */
  .ops-cr { width: 4.6em; flex: none; font-family: var(--font-mono);
    font-size: 0.7rem; text-align: right; padding: 3px 6px; border-radius: 6px;
    color: var(--text-2); background: transparent; border: 1px solid transparent; }
  .ops-cr:hover { border-color: var(--border); color: var(--text); }
  .ops-cr[readonly] { cursor: default; }
  .ops-cr:not([readonly]) { background: var(--surface-2); border-color: var(--accent);
    color: var(--text); cursor: text; }
  /* INFINITE IS A GLYPH AND NOT A FIGURE. A `quota.exempt` holder is not
     counted, and any number here would read as a balance about to run out. */
  .ops-cr.is-free { text-align: right; color: var(--text-3); font-size: 0.86rem;
    line-height: 1.2; }

  /* ---- the role editor ----------------------------------------------------
     A checklist, and the only screen in the archive where the permission table
     is the CONTENT rather than something acting on it. So it is dense on
     purpose: forty-six ticks in groups, readable at a glance, because the
     question somebody comes here with is "what does this role actually do"
     and an answer that needs scrolling is not an answer. */
  .rl-row { cursor: pointer; }
  .rl-row[aria-expanded="true"] { background: var(--glass-hi); }
  /* The markers. `sov` is not a badge for its own sake — it is the one row
     whose checklist is inert, and it has to say so before somebody clicks. */
  .rl-tag { font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.04em;
    text-transform: uppercase; padding: 2px 6px; border-radius: 999px;
    background: var(--surface-3); color: var(--text-3); }
  .rl-tag.sov { background: var(--accent-soft); color: var(--accent); }
  .rl-ed { margin: 8px 0 2px; padding: 12px 13px; border-radius: 10px;
    background: var(--surface-2); border: 1px solid var(--border); }
  .rl-ed-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
    margin-bottom: 10px; }
  .rl-ed-head input { flex: 1 1 160px; min-width: 0; background: var(--surface);
    border: 1px solid var(--border); border-radius: 8px; color: var(--text);
    font: inherit; font-size: 0.82rem; padding: 6px 9px; }
  .rl-ed-head input:disabled { opacity: 0.5; }
  .rl-why { margin: 0 0 10px; font-size: 0.74rem; line-height: 1.6; color: var(--text-3); }
  .rl-why b { color: var(--text-2); font-weight: 600; }
  /* Columns rather than one long list: the groups are small and there are a
     lot of them, so a single column is four screens of mostly whitespace. */
  .rl-groups { columns: 200px; column-gap: 18px; }
  .rl-grp { break-inside: avoid; margin-bottom: 11px; }
  .rl-grp > b { display: block; font-family: var(--font-mono); font-size: 0.64rem;
    letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-3);
    margin-bottom: 4px; }
  .rl-cap { display: flex; gap: 7px; align-items: flex-start; padding: 2px 0;
    font-size: 0.76rem; line-height: 1.45; color: var(--text-2); cursor: pointer; }
  .rl-cap input { margin: 2px 0 0; width: 14px; height: 14px; flex: none;
    accent-color: var(--accent); }
  /* A tick somebody may not give is shown and disabled, never hidden: the
     vocabulary is the same size for everybody, and a shorter list would read
     as the archive having fewer powers than it has. */
  .rl-cap.no { opacity: 0.42; cursor: not-allowed; }
  /* One tick in the whole vocabulary removes a restriction rather than adding
     a power, and it is the one that quietly defeats every gate. Marked, not
     refused — the tooltip carries the sentence, from the server. */
  .rl-cap.warn code { color: var(--text); }
  .rl-warn { font-family: var(--font-mono); font-size: 0.62rem; font-weight: 700;
    color: #ffb3b3; padding: 0 3px; }
  .rl-cap code { font-family: var(--font-mono); font-size: 0.7rem; }
  .rl-acts { display: flex; gap: 8px; align-items: center; margin-top: 12px;
    flex-wrap: wrap; }
  .rl-acts .rl-spacer { margin-left: auto; }
  .rl-dirty { font-size: 0.72rem; color: var(--accent); }

  /* ---- the transcription queue -------------------------------------------
     A queue of minutes, not seconds. The row that matters is whichever one is
     running, so it is the one that looks different. */
  .tq-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    margin-bottom: 12px; }
  .tq-state { font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.04em;
    text-transform: uppercase; padding: 2px 7px; border-radius: 5px;
    background: var(--surface-2); color: var(--text-3); }
  .tq-state.is-on { background: var(--accent-soft); color: var(--accent); }
  .tq-state.is-off { background: color-mix(in srgb, var(--tl-not) 16%, transparent);
    color: var(--tl-not); }
  .tq-row { display: grid; grid-template-columns: auto 1fr auto; gap: 10px;
    align-items: center; padding: 8px 11px; border-radius: 9px;
    background: var(--glass-sunk); margin-bottom: 6px; font-size: 0.8rem; }
  /* The one actually burning CPU right now. It has to read as MORE than the
     rows waiting behind it, which is what went wrong the first time: --live is
     the platform colour and exists only inside a stream card, so out here every
     rule holding it was invalid and the live row was the one row with no
     background at all. */
  .tq-row.is-live { background: var(--accent-soft);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 34%, transparent); }
  .tq-row.is-paused { opacity: 0.55; }
  .tq-what { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .tq-why { color: var(--tl-not); font-size: 0.72rem; }
  .tq-tag { font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.04em;
    text-transform: uppercase; padding: 2px 6px; border-radius: 5px;
    background: var(--surface-2); color: var(--text-3); white-space: nowrap; }
  .tq-tag.run { background: color-mix(in srgb, var(--accent) 26%, transparent);
    color: var(--accent); }
  .tq-acts { display: flex; gap: 5px; }
  .tq-acts button { padding: 3px 9px; border-radius: 7px; border: 1px solid var(--border);
    background: none; color: var(--text-2); font: inherit; font-size: 0.72rem; cursor: pointer; }
  .tq-acts button:hover { background: var(--surface-2); color: var(--text); }
  .tq-acts button.danger:hover { color: var(--tl-not); border-color: var(--tl-not); }
  /* `.tq-dark` STOOD HERE and is deleted with the card that drew it: the
     "whisper is not set up yet" banner. Its one sentence is `ml-why` on the
     whisper row now, off the same `whisperReady()` answer from the same
     endpoint — so the banner was the copy, not the original.
     `.tq-head`, `.tq-state`, `.tq-row` and `.tq-acts` all STAY. The rows moved
     into the Models card and kept their look, and the head and the state line
     are still the worker queue's, which has no Models row to be a second copy
     of. */

  /* ---- the models --------------------------------------------------------
     One row per task, and every row says the same four things in the same
     order: what it reads, which model does it, whether that model is here, and
     the switch. A card where each machine explained itself differently would
     be three cards. */
  .ml-row { padding: 12px 13px; border-radius: 11px; background: var(--glass-sunk);
    margin-bottom: 8px; }
  .ml-top { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
  .ml-label { font-weight: 650; font-size: 0.86rem; }
  .ml-state { font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.04em;
    text-transform: uppercase; padding: 2px 6px; border-radius: 5px;
    background: var(--surface-2); color: var(--text-3); white-space: nowrap; }
  /* Loaded is the only green: installed means it is on a disk somewhere, which
     is not the same as ready to answer in the next second. */
  .ml-state.loaded { background: color-mix(in srgb, var(--accent) 26%, transparent);
    color: var(--accent); }
  .ml-state.installed { background: color-mix(in srgb, var(--tl-meta) 20%, transparent);
    color: var(--tl-meta); }
  .ml-state.error { background: color-mix(in srgb, var(--tl-not) 16%, transparent);
    color: var(--tl-not); }
  .ml-pick { display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
    margin-top: 9px; }
  /* No fill, and the corners fully round. The picker is a LINE OF TEXT that
     happens to be changeable — the model's name is the thing being read, and a
     filled box around it made it the loudest element on a row whose actual
     subject is the name above it. The pill is what keeps it reading as a
     control at all once the fill is gone. */
  /* FLEX-GROW RATHER THAN A FIXED WIDTH, which is what makes all four rows
     line up: the picker takes whatever slack the row has, and since the three
     controls after it are identical widths, every picker ends up the same
     width and every switch at the same x. A hand-set width would have done it
     too and then been wrong at the next card size. */
  .ml-pick select { flex: 1 1 auto; min-width: 0; padding: 4px 12px;
    border-radius: 999px; border: 1px solid var(--border);
    background: none; color: var(--text); font: inherit; font-size: 0.76rem;
    max-width: 100%; }
  .ml-pick select:hover { border-color: var(--text-3); }
  /* Exactly a `.btn-ent`, because that is what it stands in for. */
  .ml-gap { flex: none; width: 30px; height: 30px; }
  .ml-pick select:disabled { opacity: 0.55; cursor: not-allowed; }
  .ml-note { margin-top: 7px; color: var(--text-3); font-size: 0.73rem; line-height: 1.5; }
  .ml-why { margin-top: 6px; color: var(--tl-not); font-size: 0.73rem; }
  .ml-q { margin-top: 7px; font-family: var(--font-mono); font-size: 0.66rem;
    color: var(--text-3); }
  /* ── EACH MODEL'S OWN LIST, UNDER ITS OWN ROW ──────────────────────────
     SHORTER THAN THE CARD IT REPLACES, and that is the one thing the merge
     had to get right: the old Model queue scrolled at about eight rows
     because it was the only list on the screen. There are three of them now,
     and three eight-row scrollers is a card that pushes the Log out of reach
     — which is the complaint `.ops-scroll`'s own comment was written about.
     About four rows each, and `.ml-q` above says how many there really are.
     NO STICKY HEAD, unlike `.ops-scroll`: the one thing a head would carry is
     whose list this is, and the row's own label is three lines above it. */
  /* ── AND IT FOLDS AWAY, SHUT TO BEGIN WITH ────────────────────────────
     THE COUNT LINE IS THE HANDLE rather than a control beside one. `259
     waiting · 4 failed` is the sentence somebody reads first and the list is
     the detail behind it, which is what `<summary>` means — so the line keeps
     its own `.ml-q` look and gains a caret and a cursor.
     SHUT, because three lists open at once is the card B25 capped the height
     of rather than solved: the counts answer "is it getting through its list"
     on their own, and the rows are for the time that answer is no.
     NO MARKER FROM THE BROWSER, following `.tok-more`'s precedent one card
     over — the default triangle sits outside the text and breaks the mono
     line it is part of. */
  .ml-list > summary { cursor: pointer; list-style: none; }
  .ml-list > summary::-webkit-details-marker { display: none; }
  .ml-list > summary::before { content: '\25B8'; display: inline-block;
    margin-right: 6px; transition: transform 0.15s ease; }
  .ml-list[open] > summary::before { transform: rotate(90deg); }
  .ml-list > summary:hover, .ml-list[open] > summary { color: var(--text-2); }
  .ml-queue { margin-top: 8px; max-height: 164px; overflow-y: auto;
    overscroll-behavior: contain; display: flex; flex-direction: column; gap: 6px; }
  .ml-row.is-off { opacity: 0.6; }

  /* ── THE SWITCH ────────────────────────────────────────────────────────
     An OUTLINED pill with the knob riding inside it, and a SECOND switch shape
     in this page on purpose: `.toggle` above is the wide filled pill with the
     word in the knob, wired to things you turn on to change what you are
     LOOKING at — the gallery's All, the importer's Pool. This one turns a
     machine on and off. Keeping them apart is the point; if a third thing
     wants a switch, the question to ask is which of those two it is.
     NOT CALLED `.toggle`, which is taken by that one and would have rewritten
     both of its users.

     A RAIL AND NOT A RING, which is the shape it ended up at. It was an
     outlined pill — a 2px ring with the knob riding INSIDE it — and the pill
     enclosed 24px of air whose only job was to be a boundary. Beside a pause
     button that has just lost its own box, that boundary was the loudest
     thing in the row and it was drawing a container rather than a control.
     So the ring becomes a line: same 44px of travel, same 14px knob, same two
     tokens, and nothing enclosed.

     THE TWO-TONE IS KEPT AND INVERTED, which is the one thing carried over
     whole. The shape as drawn has a pale ring and a dark knob, which is a
     light-background reading; on this page the rail recedes and the KNOB is
     what the eye lands on, so the knob is the brighter of the two. Same
     tokens as before — no new colour, and the rail keeps the 55% that stopped
     it competing with the knob for one glance.

     THE KNOB TRAVELS THE WHOLE WIDTH NOW, 0 to 30 rather than 5 to 25. A ring
     has an inside to sit in; a line does not, and a knob inset from the end of
     a line it overhangs reads as a line that stops early.

     CENTRED OFF 50% RATHER THAN A TOP OFFSET, because the rail is 2px and the
     knob is 14: a hand-set `top` would be two numbers that have to be kept in
     step with each other and with the 24px box, and the first resize would
     break it quietly. */
  .switch { position: relative; width: 44px; height: 24px; flex: none;
    margin-left: auto; display: flex; align-items: center; }
  .switch input[type="checkbox"] { position: absolute; left: 0; top: 0; z-index: 10;
    width: 100%; height: 100%; margin: 0; cursor: pointer; opacity: 0; }
  /* A BOX WITH A HEIGHT, which the flex version did not need and this does:
     both marks are absolute and centred against it, so a label that collapsed
     to the rail's own 2px would put the knob 1px tall worth of nowhere. */
  .switch label { position: relative; display: block; width: 44px; height: 24px;
    pointer-events: none; }
  .switch label::before { content: ''; position: absolute; left: 0; top: 50%;
    width: 44px; height: 2px; margin-top: -1px; border-radius: 999px;
    background: var(--text-3); transition: background 0.2s ease-in; }
  .switch label::after { content: ''; position: absolute; top: 50%; left: 0;
    margin-top: -7px; width: 14px; height: 14px; border-radius: 50%; z-index: 2;
    background: var(--text-2); box-shadow: 0 0 5px rgba(0, 0, 0, 0.13);
    transition: background 0.2s ease-in, left 0.2s ease-in; }
  .switch input[type="checkbox"]:hover + label::after {
    box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.13), 0 3px 8px 0 rgba(0, 0, 0, 0.07); }
  /* ON THE WHOLE SWITCH, where it used to be on the ring. An outline around a
     2px line is a sliver nobody can see, and the thing being focused is the
     control rather than either of its two marks. */
  .switch:has(input[type="checkbox"]:focus-visible) {
    outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 999px; }
  .switch input[type="checkbox"]:checked + label::before {
    background: color-mix(in srgb, var(--accent) 55%, transparent); }
  .switch input[type="checkbox"]:checked + label::after {
    background: var(--accent); left: 30px; }
  /*  The colour still changes; the TRAVEL does not. `::before` transitions
      `background` now rather than `border-color`, which is the property the
      rail actually carries its state in. */
  @media (prefers-reduced-motion: reduce) {
    .switch label::before { transition: background 0.2s ease-in; }
    .switch label::after { transition: background 0.2s ease-in; }
  }

  /* Not having a sidecar is the expected state today, so it is said once at
     the top in the same voice the transcription card uses for whisper, rather
     than three times as three errors. */
  .ml-dark { padding: 11px 13px; border-radius: 10px; font-size: 0.78rem;
    margin-bottom: 10px;
    background: color-mix(in srgb, var(--tl-meta) 12%, transparent); color: var(--text-2); }
  .ml-dark code { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text); }

  /* ---- the recorder's queue ----------------------------------------------
     The same row idiom as the transcription card above, because it is the same
     thing to look at: a short list where one row is moving. What is different
     is the alarm. That queue is worked by this process, so it is either
     running or the whole archive is down; this one is worked by a machine on
     the other side of the house that can be off, unplugged, or pointed at
     something else entirely — and none of those look like an error from here,
     they look like a queue that is simply not going down. */
  .rq-row { grid-template-columns: auto auto 1fr auto; }
  .rq-kind { font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.05em;
    text-transform: uppercase; padding: 2px 6px; border-radius: 5px; white-space: nowrap;
    background: var(--surface-2); color: var(--text-3); }
  /* Coloured by what they cost. Blue spends the recorder's bandwidth, yellow
     moves a file, red destroys one. */
  .rq-kind.fetch { color: var(--tl-character);
    background: color-mix(in srgb, var(--tl-character) 18%, transparent); }
  .rq-kind.promote { color: var(--tl-meta);
    background: color-mix(in srgb, var(--tl-meta) 18%, transparent); }
  .rq-kind.purge { color: var(--tl-not);
    background: color-mix(in srgb, var(--tl-not) 18%, transparent); }
  .rq-url { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-2); }
  .rq-warn { padding: 10px 12px; border-radius: 10px; font-size: 0.78rem;
    margin-bottom: 10px; color: #ffd6d6;
    background: color-mix(in srgb, var(--tl-not) 13%, transparent);
    border: 1px solid color-mix(in srgb, var(--tl-not) 28%, transparent); }
  .rq-warn b { color: #fff; }
  /* On the header line, where "asks for x" has to be readable as stale. A
     poll older than the claim hold means the worker is not asking at all, and
     that is not a detail to print in the same grey as the rest. */
  .rq-cold { color: var(--tl-not); font-weight: 600; }
  /* Not an alarm — nothing is broken, something was never asked for. Same
     shape as the warning so it reads as the same kind of thing to act on,
     in the colour of a note rather than of a problem. */
  .rq-fix { padding: 10px 12px; border-radius: 10px; font-size: 0.78rem;
    margin-bottom: 10px; color: var(--text-2); display: flex; gap: 12px;
    align-items: center; flex-wrap: wrap;
    background: color-mix(in srgb, var(--tl-meta) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--tl-meta) 26%, transparent); }
  .rq-fix b { color: var(--text); }
  .rq-fix button { margin-left: auto; padding: 5px 12px; border-radius: 8px;
    border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
    font: inherit; font-size: 0.78rem; cursor: pointer; white-space: nowrap; }
  .rq-fix button:hover { background: var(--surface-3); }
  /* ---- the backups -------------------------------------------------------
     The card is the worker queue's clothes — `.tq-head`, `.tq-state`,
     `.tq-row`, `.tq-links` — because it is the same kind of card and a second
     vocabulary for it would be a second thing to keep in step. Two rules are
     its own: the strip in the colour of a problem rather than of a note, for
     the two states that ARE one (no copy exists, the last one failed), and
     the line under the rows that says where they are. */
  .rq-fix.is-bad { color: var(--text-2);
    background: color-mix(in srgb, var(--tl-not) 12%, transparent);
    border-color: color-mix(in srgb, var(--tl-not) 30%, transparent); }
  .rq-fix.is-bad b { color: var(--tl-not); }
  .bk-foot { margin-top: 8px; color: var(--text-3); font-family: var(--font-mono);
    font-size: 0.66rem; overflow-wrap: anywhere; }

  /* ---- where pictures may come from --------------------------------------
     THE ROW IS THE FORM, so the inputs have to read as text until they are
     touched: five boxes per row drawn as boxes is a wall, and this list is
     read far more often than it is edited. Border on hover, border and a
     surface on focus. The add row is the same grid so its fields line up
     under the ones they will become. */
  .dc-row, .dc-new { display: grid; gap: 8px; align-items: center;
    grid-template-columns: 168px minmax(0, 1fr) minmax(0, 1.5fr) auto auto; }
  .dc-row { padding: 6px 9px; border-radius: 10px; background: var(--glass-sunk);
    margin-bottom: 6px; }
  /* Allowed, and not named to anybody. Dimmed rather than badged: it is a
     quieter state, not a worse one. */
  .dc-row.is-quiet { opacity: 0.68; }
  .dc-row.is-quiet:hover, .dc-row.is-quiet:focus-within { opacity: 1; }
  .dc-id { font-family: var(--font-mono); font-size: 0.66rem; color: var(--text-3);
    user-select: all; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .dc-row input[type="text"], .dc-new input[type="text"] { width: 100%;
    padding: 5px 8px; border-radius: 7px; border: 1px solid transparent;
    background: none; color: var(--text); font: inherit; font-size: 0.8rem; }
  .dc-new input[type="text"] { border-color: var(--border); }
  .dc-row input[type="text"]:hover { border-color: var(--border); }
  .dc-row input[type="text"]:focus, .dc-new input[type="text"]:focus {
    border-color: var(--accent); background: var(--surface-2); outline: none; }
  .dc-see { display: flex; align-items: center; gap: 5px; white-space: nowrap;
    font-size: 0.72rem; color: var(--text-3); cursor: pointer; }
  /* The browser default is a blue nothing else on this page is. */
  .dc-see input { width: 14px; height: 14px; accent-color: var(--accent);
    margin: 0; cursor: pointer; }
  .dc-x { border: 1px solid transparent; background: none; color: var(--text-3);
    font: inherit; font-size: 1rem; line-height: 1; padding: 3px 8px;
    border-radius: 7px; cursor: pointer; }
  .dc-x:hover { color: var(--tl-not); border-color: var(--tl-not); }
  .dc-new { margin-top: 10px; padding: 6px 9px; }
  /* Narrow: the id gets its own line above the fields it identifies, which is
     the one ordering that still reads as one row. */
  @media (max-width: 760px) {
    .dc-row, .dc-new { grid-template-columns: minmax(0, 1fr) auto auto; }
    .dc-id, .dc-new [data-dcnid] { grid-column: 1 / -1; }
  }

  /* The link check, in the queue's own header rather than in the repair strip
     above it: this one is routine, so it is always there and sits to the right
     of whatever the worker is saying about itself. Same clothes as `Catch up`,
     because they are the same kind of press. */
  .tq-links { margin-left: auto; padding: 5px 12px; border-radius: 8px;
    border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
    font: inherit; font-size: 0.78rem; cursor: pointer; white-space: nowrap; }
  .tq-links:hover { background: var(--surface-3); }

  /* The archive-upload matcher: a url and a verb on one line, under the
     capture tiles. Same proportions as the fields above it so the form does
     not change shape at the bottom. */
  .rec-sync { display: flex; gap: 6px; margin-top: 10px; }
  .rec-sync input { flex: 1 1 auto; min-width: 0; padding: 6px 8px; border-radius: 8px;
    border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
    font: inherit; font-size: 0.78rem; }
  .rec-sync button { flex: 0 0 auto; padding: 6px 12px; border-radius: 8px; border: 0;
    background: var(--glass-2); color: var(--text-2); font: inherit;
    font-size: 0.78rem; font-weight: 600; cursor: pointer; }
  .rec-sync button:hover { background: var(--glass-hi); color: var(--text); }
  .rec-sync button:disabled { opacity: 0.4; cursor: not-allowed; }

  /* ---- the log ------------------------------------------------------------
     One line per thing somebody did, in the order they did it. Monospace for
     the clock and nothing else: the timestamp is the column being scanned, and
     the sentence beside it is being read. */
  .lg { display: flex; flex-direction: column; gap: 2px; }
  .lg-row { display: grid; grid-template-columns: auto 1fr; gap: 12px;
    align-items: baseline; padding: 6px 9px; border-radius: 8px; font-size: 0.8rem;
    line-height: 1.5; }
  .lg-row:nth-child(odd) { background: var(--glass-sunk); }
  .lg-when { font-family: var(--font-mono); font-size: 0.66rem; color: var(--text-3);
    white-space: nowrap; }
  .lg-what { color: var(--text-2); }
  .lg-what b { color: var(--text); font-weight: 600; }
  /* The role at the time, which is a fact about the event rather than about
     the person — somebody promoted last week did not do last month's thing as
     an admin. */
  .lg-role { font-family: var(--font-mono); font-size: 0.56rem; letter-spacing: 0.06em;
    text-transform: uppercase; padding: 1px 5px; border-radius: 4px;
    background: var(--surface-2); color: var(--text-3); margin-left: 5px; }
  .lg-role.is-admin { background: color-mix(in srgb, var(--tl-not) 16%, transparent);
    color: var(--tl-not); }
  /* A machine, and worth reading as one at a glance. The recorder does more to
     this archive than most accounts ever will, and a line that says SYSTEM
     rather than VIEWER is the difference between "the Pi promoted this" and
     "somebody who could not have promoted this promoted it". */
  .lg-role.is-machine { background: color-mix(in srgb, var(--accent) 14%, transparent);
    color: var(--accent); }
  /* A pivot. Underlined on hover rather than always, because a history panel
     is mostly names and forty permanent underlines is a mess — but the cursor
     and the title say it is live before you click anything. */
  .lg-piv { cursor: pointer; text-decoration: underline dotted
      color-mix(in srgb, currentColor 40%, transparent); text-underline-offset: 2px; }
  .lg-piv:hover, .lg-piv:focus-visible { text-decoration: underline solid currentColor;
    color: var(--accent); }
  b.lg-piv:hover, b.lg-piv:focus-visible { color: var(--accent); }
  /* Each value is a token, not a run of words. "with Tenma Maemi Pipkin
     Pippa" is two taglets and read as one; a bounded chip is what tells them
     apart without a comma that a name could itself contain. */
  .lg-q { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text);
    background: var(--surface-2); padding: 1px 5px; border-radius: 4px;
    white-space: nowrap; }
  .lg-gone { color: var(--text-3); font-style: italic; }
  /* Destructive verbs are the reason this exists. Marked, not shouted. */
  .lg-row.is-loud { background: color-mix(in srgb, var(--tl-not) 9%, transparent); }
  .lg-row.is-loud .lg-what b { color: var(--tl-not); }
  .lg-more { margin-top: 12px; }
  .lg-filters { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
    margin-bottom: 12px; }
  .lg-filters select { background: var(--surface-2); border: 1px solid var(--border);
    color: var(--text); font: inherit; font-size: 0.76rem; padding: 4px 8px;
    border-radius: 7px; }

  /* The per-snippet panel. Floating and non-blocking, the same language as the
     upload window: reading a history is something you do WHILE looking at the
     thing it is about, so it must not cover it or demand dismissal. */
  .hist-win { position: fixed; z-index: 70; width: min(560px, calc(calc(var(--vw) * 100) - 32px));
    max-height: min(calc(var(--vh) * 70), 620px); display: flex; flex-direction: column;
    border-radius: 16px; border: 1px solid var(--glass-line);
    /* Nearly opaque, unlike the upload window. That one floats over the page's
       own background and can afford to be barely there; this lands directly on
       top of a snippet row, and small text over a poster and somebody else's
       title is not reading material. The glass stays in the border and the
       shadow, where it costs nothing. */
    background: color-mix(in srgb, var(--panel) 97%, transparent);
    /* No backdrop-filter, deliberately, and there never effectively was one:
       it read `blur(var(--glass-blur))`, and --glass-blur already carries its
       own function list, so that expanded to `blur(blur(26px) saturate(165%))`
       — invalid, and therefore dropped. Which is what the paragraph above asks
       for anyway. Blurring 97% opacity would cost a compositor pass to change
       nothing visible. */
    box-shadow: var(--glass-shadow); }
  .hist-head { display: flex; align-items: center; gap: 10px; padding: 13px 15px 11px;
    border-bottom: 1px solid var(--glass-line); }
  .hist-head b { font-size: 0.88rem; }
  .hist-head .sub { color: var(--text-3); font-size: 0.72rem; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .hist-x { margin-left: auto; border: 0; background: none; color: var(--text-3);
    cursor: pointer; font-size: 1rem; line-height: 1; padding: 2px 4px; }
  .hist-x:hover { color: var(--text); }
  .hist-body { overflow-y: auto; padding: 10px 12px 14px; }

  /* A gate word, wherever it appears. Same shape held by a taglet and by a
     person, because it is the same word — that is the whole mechanism, and two
     different-looking chips would hide it. */
  .ops-key { display: inline-flex; align-items: center; gap: 5px; padding: 2px 4px 2px 8px;
    border-radius: 6px; font-family: var(--font-mono); font-size: 0.68rem;
    background: color-mix(in srgb, var(--tl-not) 14%, transparent); color: var(--tl-not); }
  .ops-key button { border: 0; background: none; color: inherit; cursor: pointer;
    font: inherit; opacity: 0.6; padding: 0 3px; border-radius: 4px; }
  .ops-key button:hover { opacity: 1; background: color-mix(in srgb, var(--tl-not) 22%, transparent); }

  .ops-give { margin-left: auto; display: flex; gap: 6px; align-items: center; }
  /* ONE RULE FOR EVERY CONTROL ON THIS SCREEN, and the users panel was never
     in it. `.us-cell > select` and `.us-keys select` set a width and nothing
     else, so the rank dropdown and the gate dropdown have been rendering in
     the browser's own chrome — white box, system font, light text on a light
     background — on a panel that is otherwise entirely dark. It reads as a
     piece of the page that failed to load.
     Not noticed for as long as it has existed because both are closed almost
     always: they are a word until somebody clicks them, which is the whole
     design of the row. The give form opened two at once and made it obvious. */
  .ops-give select, .ops-add input,
  .us-give select, .us-give .us-pick, .us-cell > select, .us-keys select {
    background: var(--surface-2); border: 1px solid var(--border);
    color: var(--text); font: inherit; font-size: 0.76rem; padding: 4px 8px; border-radius: 7px; }
  .ops-give select:disabled, .us-give select:disabled { opacity: 0.4; }
  .us-give select:focus, .us-give .us-pick:focus, .us-cell > select:focus {
    outline: none; border-color: var(--accent); }
  /* THE INVITE AND REFILL FORMS are `.ops-give` rows that are not pushed to
     the right: that class exists to park a control at the end of a row it
     shares with a name, and here each of them IS the row. */
  .us-top .ops-give { margin-left: 0; margin-top: 10px; }
  .us-top .ops-give input[type="text"] { flex: 1 1 auto; min-width: 0;
    background: var(--surface-2); border: 1px solid var(--border);
    color: var(--text); font: inherit; font-size: 0.76rem; padding: 4px 8px;
    border-radius: 7px; }

  /* ---- the Users card ---------------------------------------------------
     One full-width roster. Gate ownership and Role ownership were two
     half-width lists of the SAME people, so a person's rank was in one and
     what they can see was in the other.
     INVITES AND CREDITS SIDE BY SIDE on top: both answer how somebody comes to
     be on the list, neither fills a width on its own, and `auto-fit` drops
     them into one column on a narrow screen without a second rule. */
  .us-card { margin-top: 16px; }
  .us-top { display: grid; gap: 0 28px; align-items: start;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
  .us-topcol { min-width: 0; }
  .us-topcol .rec-sec { margin-top: 6px; }
  /* HOW MANY LINKS ARE STILL LIVE, pushed past the rule by `order`: the rule
     is the `::after` and so the last child of the flex box, which a trailing
     span would otherwise sit in front of. `.rec-sec span` already gives it the
     mono and the colour. */
  .us-count { order: 1; }

  /* ONE GRID, DECLARED ONCE, USED BY THE HEADER AND EVERY ROW. The columns
     line up because they are literally the same columns -- two sets of widths
     kept equal by hand is the thing this avoids, and it is why the header is a
     row of the grid rather than a table head.
     IN `rem`, AND NOT IN `ch` OR `em`, which is what broke it the first time:
     both of those resolve against the element's OWN font, the header is set in
     0.58rem mono and the rows in the body face at 0.86rem, so one
     `grid-template-columns` produced two different sets of widths and the
     header sat visibly left of the data it named. `rem` is the same length in
     both.
     `minmax(0, …)` ON THE NAME is what makes a long handle get CUT instead of
     widening its column and pushing everything after it out of line: a grid
     track will not shrink below its content without the explicit 0. */
  .us-head, .us-row { display: grid; align-items: center; gap: 10px;
    grid-template-columns: minmax(0, 10rem) 3.4rem minmax(0, 6rem) 4rem
      minmax(0, 1fr) minmax(0, 8.5rem); }
  .us-head { padding: 2px 12px 0; color: var(--text-3);
    font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.06em;
    text-transform: uppercase; }
  .us-row { padding: 9px 12px; border-radius: 10px;
    background: var(--glass-sunk); }
  /* CUT, with the whole handle on the `title`. 20ch of a name is enough to
     recognise it and the column has to stay a column. */
  .us-who { font-weight: 600; font-size: 0.86rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .us-keys { display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
    min-width: 0; }
  .us-cell { min-width: 0; display: flex; align-items: center; }
  .us-cell > select { width: 100%; }
  /* The balance fills its track and reads left, under the word that names it.
     `.ops-cr` was built for a flex row where it sized itself. */
  .us-row .ops-cr { width: 100%; text-align: left; }
  /* GRANTING IS A `+` AT THE END OF WHAT THEY ALREADY HOLD, which is where the
     answer to "and one more" belongs -- a permanent dropdown in a column of
     its own read as a sixth thing about the person rather than as an action on
     the list beside it. */
  /* A scoped right is not a gate, and must not read as one. Tinted, with the
     capability set in small caps ahead of what it is over — the eye should
     stop on it rather than scan past a word that looks like the four beside
     it. */
  .ops-key.is-scoped { background: var(--accent-soft); color: var(--text-2);
    gap: 4px; }
  .ops-key.is-scoped i { font-style: normal; font-size: 0.56rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--accent); }
  .ops-key.is-scoped em { font-style: normal; opacity: 0.6; }

  /* The give form. Inline and compact, because it lives at the end of a row
     that is mostly read — a block of fields would make the users list look
     like something to fill in rather than something to look at. */
  .us-give { display: inline-flex; align-items: center; gap: 4px; flex-wrap: wrap; }
  /* A BUTTON WEARING THE INPUT'S SHAPE. It is the same box as the selects
     beside it, and bounded at both ends: a floor so the row does not jump
     when `pick an artist` becomes a two-letter name, a ceiling so a long one
     ellipsises instead of pushing `Assign` off the end of the cell. */
  .us-give .us-pick { min-width: 8rem; max-width: 13rem; text-align: left;
    cursor: pointer; overflow: hidden; white-space: nowrap;
    text-overflow: ellipsis; }
  .us-give .us-pick:not([data-slug]) { color: var(--text-3); }
  /* The chip inside it is a FACE, not a target: a click anywhere on the button
     reopens the picker, and a chip that swallowed it would make the middle of
     the control dead. */
  .us-give .us-pick .tl { pointer-events: none; }
  .us-give [hidden] { display: none; }

  .us-plus { width: 22px; height: 22px; flex: none; display: inline-flex;
    align-items: center; justify-content: center; padding: 0;
    border-radius: 7px; font-size: 0.8rem; line-height: 1; }
  /* The one hover that ADDS to the base instead of repeating it: the outline
     goes solid, which is this control saying the thing it makes is real. */
  .us-plus:hover { border-style: solid; }
  .us-keys select { min-width: 9em; }
  /* BAN SITS BESIDE PURGE, so they are drawn to say which is which: one is a
     column somebody flips back and the other is not. */
  .us-acts { justify-content: flex-end; gap: 6px; }
  .us-act { padding: 3px 8px; border-radius: 7px; cursor: pointer;
    font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.06em;
    text-transform: uppercase; background: none; color: var(--text-3);
    border: 1px solid var(--glass-line); }
  .us-act:hover { color: var(--text); border-color: var(--text-3); }
  .us-act.danger:hover { color: var(--tl-not); border-color: var(--tl-not); }
  /* A BANNED ROW STILL READS AS A PERSON, because unbanning is a thing you do
     to somebody you can still find. Struck through and dimmed, with the button
     beside it already saying Unban. */
  .us-row.is-banned { opacity: 0.55; }
  .us-row.is-banned .us-who { text-decoration: line-through; }

  /* THE REFILL ROW: a label, a number and a button, all three on one line.
     The label carried `.ops-note`, whose 14px bottom margin made it the
     tallest thing in the row and pushed the other two off centre, and the
     number input carried no field styling at all -- the rule above is scoped
     to `[type="text"]` so that the invite note is the only thing that grows to
     fill the row, and the number fell through it. */
  .us-refill label { color: var(--text-3); font-size: 0.76rem; }
  .us-refill input { width: 6em; flex: none; background: var(--surface-2);
    border: 1px solid var(--border); color: var(--text); font: inherit;
    font-size: 0.76rem; padding: 4px 8px; border-radius: 7px; }
  /* THE RANK READS AS TEXT AND EDITS AS A SELECT, because it is read far more
     often than it is changed -- a roster of dropdowns reads as a form to fill
     in rather than as a report with one correctable field. Same gesture the
     credit figure uses, one click rather than two because there is nothing to
     mistype here. */
  /* NOT AN ADD, and it was only ever dashed-TRANSPARENT — an outline nobody
     could see until they were on it. Two ideas were wearing one line: "this
     makes a new one" and "this is editable", and the dash is for the first.
     The hover already said the second on its own. */
  .us-rank { font-family: var(--font-mono); font-size: 0.66rem;
    letter-spacing: 0.04em; text-transform: uppercase; text-align: left;
    background: none; border: 0; border-radius: 6px;
    color: var(--text-2); font-weight: inherit; padding: 3px 5px;
    cursor: pointer; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  .us-rank:hover { color: var(--text);
    background: var(--surface-2); }
  /* A SOVEREIGN RANK IS NOT A BUTTON. There is nothing to choose: the role
     that holds everything is what makes it impossible to lock yourself out of
     your own archive, and offering a select that would take it away is
     offering the one change this screen must not make. */
  .us-rank.is-fixed { cursor: default; border-color: transparent;
    color: var(--text-3); }
  .us-rank.is-fixed:hover { background: none; border-color: transparent; }
  /* WHAT CAME BACK, and it is the only place that password will ever appear.
     Mono because it is read off the screen and typed, and selectable as one
     unit because the next thing anybody does with it is copy it. */
  .ops-made { margin-top: 10px; padding: 9px 11px; border-radius: 9px;
    border: 1px solid var(--accent); background: var(--accent-soft);
    font-size: 0.74rem; line-height: 1.6; }
  .ops-made code { display: block; margin: 6px 0 2px; font-family: var(--font-mono);
    font-size: 0.86rem; letter-spacing: 0.04em; color: var(--text);
    user-select: all; }
  .ops-made .ops-once { color: var(--text-3); font-size: 0.68rem; }
  .ops-add { position: relative; margin-top: 12px; }
  .ops-add input { width: 100%; padding: 8px 11px; }
  .ops-ac { position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 5;
    max-height: 260px; overflow-y: auto; border-radius: 10px;
    border: 1px solid var(--glass-line); background: var(--bg-blur);
    /* var(--glass-blur), not blur(var(--glass-blur)) — the var is already a
       function list, so wrapping it made `blur(blur(26px) saturate(165%))`,
       which is invalid and was being dropped. This dropdown's background is
       translucent, so the blur is what makes the names on it readable. */
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur); box-shadow: var(--glass-shadow); }
  .ops-ac button { display: flex; width: 100%; gap: 8px; align-items: center; border: 0;
    background: none; color: var(--text); font: inherit; font-size: 0.8rem;
    padding: 8px 11px; text-align: left; cursor: pointer; }
  .ops-ac button:hover { background: var(--glass-hi); }
  .ops-ac i { width: 7px; height: 7px; border-radius: 2px; flex: 0 0 auto; }
  .ops-ac .ops-sub { margin-left: auto; color: var(--text-3); font-family: var(--font-mono);
    font-size: 0.66rem; }

  /* ---- Panels ---- */
  main { padding-bottom: 80px; }
  .panel { margin-top: 26px; animation: rise 0.32s ease both; }
  .panel[hidden] { display: none; }
  @keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

  .toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-bottom: 22px; }
  .search { flex: 1 1 240px; position: relative; }
  .search input { width: 100%; background: var(--surface); border: 0; color: var(--text); font: inherit; padding: 12px 14px 12px 38px; border-radius: var(--radius-sm); }
  .search input::placeholder { color: var(--text-3); }
  .search svg { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); color: var(--text-3); }
  .meta { font-family: var(--font-mono); font-size: 0.74rem; color: var(--text-3); }
  /* THE TOKEN REDEFINED IN PLACE rather than the radius hard-set: the rule
     already reads `var(--radius-sm)`, so moving the token here keeps one name
     for the idea and lets anything nested inherit the same corner. */
  .btn-primary { --radius-sm: 4px;
    background: var(--accent); color: var(--on-accent); border: 0; font-weight: 700; padding: 12px 16px; border-radius: var(--radius-sm); font-size: 0.88rem; }
  /* And the whole audit panel, for the same reason one level up: everything
     inside it that asks for the small radius gets the sharper corner without
     each rule having to be found and edited. */
  #tp-body[data-tool="audit"] { --radius-sm: 4px; }
  .btn-ghost { background: var(--surface-2); color: var(--text-2); border: 0; padding: 8px 12px; border-radius: 8px; font-size: 0.82rem; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; }
  .btn-ghost:hover { background: var(--surface-3); color: var(--text); }
  .icon-btn { background: var(--surface-2); border: 0; color: var(--text-2); width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; }
  .icon-btn:hover { background: var(--surface-3); color: var(--text); }

  .pill { font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.04em; padding: 3px 7px; border-radius: 6px; background: var(--surface-2); color: var(--text-2); }
  .pill.accent { background: var(--accent-soft); color: var(--accent); }
  /* The mark carries the colour, not the pill: a row of tinted boxes reads as
     five warnings, and three of them are good news. */
  .pill .mk { font-weight: 700; font-style: normal; }
  .pill .mk.ok { color: var(--accent); }
  .pill .mk.gone { color: #ffb3b3; }
  .pill .mk.none { color: var(--text-3); }
  /* Present but not playable — matching .st-ico.raw. Without this the mark
     inherited the pill's own colour, which is the same grey `none` uses, and
     "raws waiting to be merged" would have looked identical to "nothing was
     ever captured" on the one surface that draws a pill. */
  .pill .mk.raw { color: var(--text-2); }
  .chip { font-size: 0.78rem; padding: 5px 10px; border-radius: 8px; background: var(--surface-2); color: var(--text-2); cursor: pointer; }
  .chip:hover { background: var(--surface-3); color: var(--text); }

  /* placeholder media block */
  .ph { background: var(--surface-2); position: relative; overflow: hidden; }
  .ph::after { content: ""; position: absolute; inset: 0; background: linear-gradient(125deg, transparent 40%, rgba(255,255,255,0.035) 50%, transparent 60%); }
  .ph .glyph { position: absolute; inset: 0; display: grid; place-items: center; color: var(--text-3); opacity: 0.5; }

  /* interactive items */
  .item { cursor: pointer; transition: filter 0.15s ease, background 0.15s ease; }
  .tile.item:hover, .vod.item:hover .thumb { filter: brightness(1.12); }
  .clip.item:hover, .vod.item:hover { background: var(--surface-2); }

  /* Image grid */
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
  .tile { aspect-ratio: 1; border-radius: var(--radius-sm); }
  .tile .tag-corner { position: absolute; top: 7px; left: 7px; z-index: 1; }

  /* Snippets list */
  .clips { display: flex; flex-direction: column; gap: 10px; }

  /* ---- snippets ----------------------------------------------------------
     Three columns: the clip, what it is, what was said. The transcript is a
     fixed-height scroller rather than a growing block, so a row is the same
     height whether the clip said two words or forty — a list where every row
     is a different height is a list you cannot skim. */
  .snips { display: flex; flex-direction: column; gap: 10px; }
  /* The middle column holds a title and a handful of chips; the right one holds
     everything that was said. Weighted accordingly — an even split leaves the
     body column half empty and the transcript wrapping every line. */
  .snip { position: relative; display: grid; gap: 14px; align-items: start;
    grid-template-columns: 210px minmax(0, 0.85fr) minmax(0, 1.35fr);
    padding: 12px; background: var(--panel); border-radius: var(--radius);
    border: 1px solid transparent; transition: border-color 0.15s ease; }
  /* NOTHING TO TRANSCRIBE, so the track it would have taken is the body's.
     Columns and not a span, because the base places its children by flow and
     a third track with no item in it still claims its share of the free space
     — an fr is a share of what is left, not of what is used. */
  .snip.no-tr { grid-template-columns: 210px minmax(0, 1fr); }
  /* The OCR fold. Borrowing `.ops-fold`'s summary idiom rather than inventing
     a second one — two collapsibles that behave differently is one more thing
     to learn — and read-only: there is no seeking into a drawing. */
  .snip-ocr { margin-top: 8px; }
  .snip-ocr > summary { cursor: pointer; padding: 3px 0; list-style: none;
    font-family: var(--font-mono); font-size: 0.63rem; font-weight: 500;
    letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-3);
    display: flex; align-items: center; }
  .snip-ocr > summary::-webkit-details-marker { display: none; }
  .snip-ocr > summary:hover { color: var(--text-2); }
  .snip-ocr > summary .ops-n { margin-left: 8px; }
  .snip-ocr[open] > summary { margin-bottom: 4px; }
  .snip-ocr-body { max-height: 128px; overflow: auto; font-size: 0.78rem;
    line-height: 1.55; color: var(--text-2); user-select: text; }
  /* THE RATING, AS A CONTROL. A <select> and not the archive's own popover,
     for once: four fixed words with one current value is the shape a select
     IS, it arrives keyboard- and screen-reader-correct for free, and a custom
     menu here would be a second thing to learn for no gain.
     IT CARRIES `.au-rate` TOO, rather than restating a look beside it. The
     first version invented its own flat grey and lost the only thing the chip
     was saying: `general` and `sensitive` are uncoloured on purpose, because
     colouring every row teaches the eye to stop reading them, and the two that
     would make somebody look twice are the two that are coloured. A control
     that drops that is a control that says less than the label it replaced. */
  .rate-pick { appearance: none; -webkit-appearance: none; cursor: pointer;
    /* Room for the caret, and the caret itself — a <select> cannot carry a
       ::after, so it is painted rather than placed. Quiet: the pill is the
       thing being read, and the caret is only there so it is known to open. */
    padding-right: 16px;
    background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 5px center;
    background-size: 7px 5px; }
  .rate-pick:hover { border-color: var(--text-3); }
  .rate-pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
  /* Not a value, so not coloured like one. */
  .rate-pick.is-mixed { font-style: italic; text-transform: none; }
  /* BY HAND, SAID WITHOUT TAKING THE COLOUR. The first version put the accent
     on the border, which fought `explicit`'s red for the one slot that was
     carrying the warning. Weight is free. */
  .rate-pick.is-mine { font-weight: 700; }
  /* The option list is the browser's, which cannot be styled far — what it
     can be given is a background that is not white in a dark page. */
  .rate-pick option { background: var(--surface); color: var(--text);
    text-transform: none; font-weight: 400; }
  .snip:hover { border-color: var(--surface-3); }
  /* ══ THE QUEUE, AS A GRID ═══════════════════════════════════════════════
     Two modes of ATTENTION rather than two layouts, which is why this is worth
     having at all. "Is this worth keeping, and does it belong with that one"
     is answered by LOOKING, side by side, with no prose in the way. "What is
     this, what is it tagged, who made it" is answered by READING. The panel
     only ever had the reading layout, so the looking was done by scrolling
     past a lot of words.

     A CLASS AND NOT A SECOND RENDERER, which is the whole reason this is
     small. The card is already a grid with named areas, so the tile is the
     `media` area alone with the rest folded away, and every control, mark,
     badge and selection already on the row keeps working because it is the
     same element. Switching costs nothing: no refetch, no re-render, nothing
     loses its place, and the class lives on the LIST so it survives a reload
     for free. A second renderer would have been a second thing to keep in
     step with every future row type, and songs get a tile here for nothing.

     THE AREAS HAVE TO GO WITH THE COLUMNS, which took one wrong build to
     learn. `grid-template-columns` alone did nothing: `.snip.hasv` names four
     areas, and a template whose areas still say "media body tr verdict"
     builds four columns whatever the column list says. The media collapsed to
     ZERO WIDE and the transcript column took the tile. So the tile re-declares
     the areas down to one, and the children that are not the picture are
     folded by name rather than by hoping they were not placed.

     AND IT IS `:not(.is-rvopen)`, so a tile opened out is not a tile with
     exceptions bolted back on — it is a CARD again, spanning the row, under
     the panel's own rules. One selector does the whole of "open it back up",
     and a future change to the card's layout follows into it for nothing.

     IT DOES NOT COLLAPSE SEQUENCES, which is the one place it deliberately
     parts company with the gallery. A grid of pictures that folds a pool into
     a deck is right when you are browsing an archive and wrong when you are
     deciding what belongs with what: the members have to stay visible and
     where they were, wearing a label, or the next judgement has nothing to be
     made against. */
  #rv-list.rv-grid { display: grid; gap: 10px; align-items: start;
    grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); }

  /* ONE AREA, AND IT IS THE PICTURE. */
  #rv-list.rv-grid .snip:not(.is-rvopen) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto;
    grid-template-areas: "media";
    gap: 0; padding: 6px; }
  /* SQUARE, because a field of mixed ratios cannot be scanned — and scanning
     is the entire job here. `is-still` already draws a picture `contain`, so
     a square box letterboxes rather than crops and nothing is hidden from the
     judgement being made about it. */
  #rv-list.rv-grid .snip:not(.is-rvopen) > .snip-media {
    grid-area: media; width: auto; aspect-ratio: 1 / 1; cursor: pointer; }
  /* THE WORDS, FOLDED — and the transcript column with them, which a clip in
     this queue has and a picture does not. Folded BY NAME: the areas above
     leave them unplaced, and an unplaced child is auto-placed into a row of
     its own rather than politely disappearing. */
  #rv-list.rv-grid .snip:not(.is-rvopen) > .snip-body,
  #rv-list.rv-grid .snip:not(.is-rvopen) > .snip-tr { display: none; }
  /* The shrink control is about a card's own height and means nothing to a
     tile — and a tile is never `.big`, see the expand guard. */
  #rv-list.rv-grid .snip:not(.is-rvopen) .snip-shrink { display: none; }

  /* VERDICTS OVER THE PICTURE, AND QUIET UNTIL THE POINTER ARRIVES. A field of
     thumbnails at 168px is also a field of buttons three pixels from the next
     tile's, and every one of them is a verdict; quiet until the pointer is
     actually on a tile is the difference between a dense surface and a
     dangerous one. Out of flow rather than hidden, so the tile is exactly the
     size of its picture and nothing moves when they appear. Focus counts, so
     the keyboard never loses them, and so does being picked — what is selected
     is what somebody is about to act on. */
  #rv-list.rv-grid .snip:not(.is-rvopen) > .rv-verdict {
    position: absolute; right: 7px; bottom: 7px; z-index: 3;
    opacity: 0; transition: opacity 0.12s ease; }
  #rv-list.rv-grid .snip:not(.is-rvopen):hover > .rv-verdict,
  #rv-list.rv-grid .snip:not(.is-rvopen):focus-within > .rv-verdict,
  #rv-list.rv-grid .snip.is-rvsel:not(.is-rvopen) > .rv-verdict { opacity: 1; }

  /* ── PICKED, IN THE GRID: A RING, AND IT IS DRAWN LAST ────────────────
     The card's pill is wrong here for the obvious reason — a 3px stripe down
     the left of a square thumbnail is not a thing anybody sees in a field of
     twelve — and the tile's own border was no better, because THE FRAME
     SCALES ON HOVER and a picture grown 16% paints straight over the edge
     that was carrying the answer. So the tile under the pointer, which is
     exactly the one being decided about, was the one that stopped showing
     whether it was picked.
     Drawn as an overlay above the picture rather than as an edge beneath it,
     and `pointer-events: none` so it is a mark and never a target. */
  #rv-list.rv-grid .snip.is-rvsel:not(.is-rvopen)::before {
    content: ''; position: absolute; inset: 0; width: auto; height: auto;
    top: 0; bottom: 0; left: 0; right: 0; z-index: 5; pointer-events: none;
    border: 2px solid var(--accent); border-radius: var(--radius);
    background: transparent;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55) inset; }

  /* AND THE SCALE ITSELF GOES. It earns its keep in a one-column list, where
     a row has empty space on both sides and growing the picture is how you
     look closer without opening anything. In a grid it has nowhere to grow
     into: every tile's neighbour is 10px away, so twelve thumbnails lunging
     at the cursor is noise, and the overlap is what hid the selection in the
     first place. The shadow stays — it is what says "this one" — and the
     verdicts coming up do the rest. */
  #rv-list.rv-grid .snip:not(.is-rvopen):hover .snip-frame,
  #rv-list.rv-grid .snip:not(.is-rvopen):focus-within .snip-frame {
    transform: none; box-shadow: 0 8px 22px rgba(0, 0, 0, 0.5); }

  /* ── THE VERDICTS, AS GLASS ───────────────────────────────────────────────
     Bigger because they are the whole point of the surface and were drawn at
     a size that suits a card's quiet right-hand column, not a thumbnail being
     judged at a glance. Tinted because green and red ARE the two answers and
     an outline-only pair makes you read the glyph to tell them apart. Frosted
     rather than solid because they sit ON the picture: an opaque chip covers
     the thing being decided about, and a blurred one stays legible over
     whatever is under it without hiding it. */
  /* STACKED, AND THE ✓ ON TOP. Side by side the pair is 92px of the
     tile's bottom edge, and the sequence badge lives at the other end of
     that same edge — on a 168px tile they met. Vertical, the column is one
     button wide and the badge has the whole bottom to itself, which is the
     fix and also the better reading: two answers above one another is a
     choice, two answers in a row is a toolbar. Yes on top because it is the
     commoner answer and because `A` is above `X` in nobody's head but the
     order still has to be stable. */
  #rv-list.rv-grid .snip:not(.is-rvopen) > .rv-verdict {
    flex-direction: column; gap: 7px; align-items: flex-end; }
  /* AND NO EDGE ON THEM. A border is how a control says where it ends when
     it has no fill of its own; these have a fill, and over a photograph a
     1px line reads as a cut-out rather than a chip. The tint and the blur
     do the whole job. `transparent` and not `border: 0`, so the box keeps
     the same size as everywhere else it is drawn. */
  #rv-list.rv-grid .snip:not(.is-rvopen) .rv-btn {
    width: 42px; height: 42px; border-radius: 8px; border-color: transparent;
    -webkit-backdrop-filter: blur(10px) saturate(1.5);
    backdrop-filter: blur(10px) saturate(1.5);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45); }
  #rv-list.rv-grid .snip:not(.is-rvopen) .rv-btn svg { width: 21px; height: 21px; }
  #rv-list.rv-grid .snip:not(.is-rvopen) .rv-yes {
    background: rgba(111, 207, 135, 0.3); border-color: transparent;
    color: #b6f0c5; }
  #rv-list.rv-grid .snip:not(.is-rvopen) .rv-yes:hover:not(:disabled) {
    background: rgba(111, 207, 135, 0.52); border-color: transparent;
    color: #e4fdeb; }
  #rv-list.rv-grid .snip:not(.is-rvopen) .rv-no {
    background: rgba(255, 107, 107, 0.28); border-color: transparent;
    color: #ffbdbd; }
  #rv-list.rv-grid .snip:not(.is-rvopen) .rv-no:hover {
    background: rgba(255, 107, 107, 0.5); border-color: transparent;
    color: #ffe9e9; }
  /* AND THE ONE THAT CANNOT ACT KEEPS ITS SILENCE, which is the behaviour
     worth protecting here: a refused publish must not be a green button with
     the colour turned down, it must barely be there. Stated against the grid
     rules above because they would otherwise tint it back in. */
  #rv-list.rv-grid .snip:not(.is-rvopen) .rv-btn:disabled,
  #rv-list.rv-grid .snip:not(.is-rvopen) .rv-btn:disabled:hover {
    opacity: 0.3; background: rgba(255, 255, 255, 0.05); color: var(--text-2);
    border-color: transparent; box-shadow: none; }

  /* AND ONE TILE, OPENED IN PLACE. Double-click spans it across the row and
     gives the words back — "what IS this one" answered without leaving the
     mode you are working in, and without the grid reflowing around a hole.
     One line, because everything above declined to touch it. */
  #rv-list.rv-grid .snip.is-rvopen { grid-column: 1 / -1; }

  /* WHICH SEQUENCE, AS A COLOUR. The id has always been drawn; comparing eight
     characters by eye across twelve tiles is the work rather than the answer,
     and a hue off the same id makes "these three are already together"
     something seen instead of read. `--pool-hue` is set per badge from
     `poolHue`. */
  .snip-pool { --pool-hue: 270; }
  #rv-list.rv-grid .snip-pool {
    color: hsl(var(--pool-hue) 70% 72%);
    background: hsl(var(--pool-hue) 55% 20% / 0.85);
    border-color: hsl(var(--pool-hue) 50% 45%); }

  /* THE DUPLICATE MARK, COMPACT. Half of "throw out the obvious dupes" is the
     archive already having said so, and the card's own block is inside the
     words that just got folded away. Drawn into the picture rather than
     restyled out of the body: a band over a thumbnail is read at a glance,
     where a row of text under one is read only if you were already reading. */
  .snip-dupflag { display: none; }
  #rv-list.rv-grid .snip:not(.is-rvopen) .snip-dupflag { display: block;
    position: absolute; left: 8px; top: 8px; z-index: 4; }
  /* ── SOLID, AND ONLY HERE ─────────────────────────────────────────────
     `.dupband` is one face in four places — the upload card, a queue row's
     block, a gallery tile and the compare overlay — and in every one of them
     it sits on a PANEL, where a 16% wash over a flat surface is perfectly
     legible. On a tile it sits on a PHOTOGRAPH, and a translucent chip over
     an arbitrary picture is whatever that picture happens to be: the word
     disappeared into light backgrounds.
     Matched to the other mark on the same tile rather than invented: the
     REVISE flag opposite is solid with dark text, and two marks on one
     picture reading as one vocabulary is the whole reason that one was made
     a word in the first place. The three colours are untouched — they are
     three different claims and the file says so where they are declared. */
  #rv-list.rv-grid .snip-dupflag.dupband { border-color: transparent;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); }
  #rv-list.rv-grid .snip-dupflag.is-same { background: var(--tl-not);
    color: #1d0b10; }
  #rv-list.rv-grid .snip-dupflag.is-resized { background: var(--tl-hold);
    color: #1b1206; }
  #rv-list.rv-grid .snip-dupflag.is-near { background: var(--tl-meta);
    color: #0e1419; }

  /* ── AND THE LEFT-HAND MARKS STACK RATHER THAN SHARE A CORNER ──────────
     A card is wide and its marks had a whole edge to spread along; a tile is
     168px square and everything that was merely NEAR something else is now
     on top of it. Two collisions, both of them long-standing pairs that a
     square was the first thing to put together:

     TOP LEFT — the collection label (`.snip[data-kind]::after`, which has
     been drawn there since the queue learned to hold more than clips) and
     the likeness band this round added. The label won on z-index and the
     band's tail stuck out from behind it, which is how it was reported.

     BOTTOM LEFT — the sequence badge and the `came from` pill, which have
     both been at `bottom: ~5px; left: ~5px` all along and have simply never
     met: a picture has to be BOTH in a sequence and linked from somewhere
     for it to happen, and that pair is rare on a card and ordinary in a
     review grid.

     THE ORDER IN EACH STACK IS "which one does not change": the label and
     the sequence badge are facts about what the row IS, so they keep the
     corner, and the marks that are about a JUDGEMENT — this looks like
     another one, this came from a channel — sit alongside them. `:has()`
     means the offset exists only when there is something to clear, so a
     picture with one mark still has it in the corner. */
  #rv-list.rv-grid .snip[data-kind]:not(.is-rvopen) .snip-dupflag { top: 31px; }
  #rv-list.rv-grid .snip:not(.is-rvopen):has(.snip-pool) .snip-from {
    bottom: 29px; }

  /* AND A TILE THAT CANNOT BE PUBLISHED, SAID IN THE CARD'S OWN WORDS. The
     approve button is already drawn disabled, which on a card sits beside a
     REVISE block naming the unanswered word — and the grid folds that block
     away, so without a mark the tile looks ready and the button reads as
     broken. Opposite the likeness band and built like it, because two marks
     in one vocabulary are read once. */
  .snip-fixflag { display: none; }
  #rv-list.rv-grid .snip:not(.is-rvopen) .snip-fixflag { display: block;
    position: absolute; right: 5px; top: 5px; z-index: 2;
    font-family: var(--font-mono); font-size: 0.56rem; letter-spacing: 0.06em;
    padding: 2px 5px; border-radius: 5px; pointer-events: none;
    color: #1b1206; background: var(--tl-hold);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); }

  /* PICKED, in a queue. The gallery dims what is not selected because a grid
     of tiles is read as a field; a queue is read one row at a time and dimming
     forty of them to show four would make the panel look broken. So the picked
     ones are marked and the rest are left alone. */
  .snip.is-rvsel { border-color: var(--accent); }
  /* INSET, AND ROUNDED AT BOTH ENDS. Full height at `left: 0` it ran straight
     past the card's own 14px corner, so the bar's square top sat outside the
     curve and read as a spur rather than a marker. Pulled in and rounded, it
     is a pill beside the row — the same idea, drawn like it belongs to the
     shape it is on. */
  .snip.is-rvsel::before { content: ''; position: absolute; left: 0;
    top: 12px; bottom: 12px; width: 3px; border-radius: 0 3px 3px 0;
    background: var(--accent); }
  /* The scaled frame has to paint over its neighbours, and a row that is only
     raised on hover keeps the stacking order stable the rest of the time. */
  .snip:hover, .snip:focus-within { z-index: 2; }

  /* The media cell keeps its layout box; only the frame inside it scales, so
     growing the picture never reflows the row or nudges the text beside it. */
  .snip-media { position: relative; width: 210px; aspect-ratio: 16 / 9; }
  .snip-frame { position: absolute; inset: 0; border-radius: 10px; overflow: hidden;
    background: var(--surface); transform: scale(1); transform-origin: center;
    transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.18s ease;
    box-shadow: 0 0 0 0 rgba(0,0,0,0); }
  .snip:hover .snip-frame, .snip:focus-within .snip-frame {
    transform: scale(1.16); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.55); }
  .snip-frame img, .snip-frame video { width: 100%; height: 100%; object-fit: cover;
    display: block; }
  /* The poster stays under the video rather than being swapped out: a video
     element paints nothing for its first frame or two, and removing the image
     first is a visible black flash on every hover. */
  .snip-frame video { position: absolute; inset: 0; }
  /* A sound clip's poster IS the content, so let it show through the letterbox
     rather than being cropped to fill a 16:9 tile. */
  .snip.is-sound .snip-frame video { object-fit: contain; }

  /* Everything inside the frame is scenery until the card is expanded, so the
     frame itself is the only hit target. This is not tidiness — mounting the
     <video> on hover swaps the topmost element under the pointer mid-gesture,
     and a mousedown and mouseup that land on different elements produce no
     click at all. Measured before this: a click 100ms or 200ms after the
     pointer arrived was silently swallowed, 30ms and 600ms were not. One
     stable target and the gesture cannot come apart.
     The video takes its clicks back when expanded, where it owns real
     controls — see .snip.big below. */
  .snip-frame img, .snip-frame video, .snip-play, .snip-dur { pointer-events: none; }
  .snip.big .snip-frame video { pointer-events: auto; }
  /* Inside the frame, so it rides the hover scale instead of sitting stranded
     at the picture's old corner. */
  .snip-dur { position: absolute; right: 4px; bottom: 4px; z-index: 3;
    font-family: var(--font-mono); font-size: 0.55rem; padding: 2px 4px;
    border-radius: 4px; background: rgba(10, 8, 14, 0.82); color: var(--text); }
  /* The same corner family as the duration, on the other side — a still has
     no duration, so the two never meet, and a clip that somehow had both
     would read as two facts rather than one overlapping smudge. */
  .snip-pool { position: absolute; left: 4px; bottom: 4px; z-index: 3;
    display: inline-flex; align-items: center; gap: 3px; pointer-events: none;
    font-family: var(--font-mono); font-size: 0.55rem; padding: 2px 5px;
    border-radius: 4px; background: rgba(10, 8, 14, 0.82); color: var(--text);
    border: 1px solid var(--accent-soft); }
  .snip-pool svg { width: 8px; height: 8px; flex: none; opacity: 0.85; }
  /* THE ONE CONTROL ON AN OTHERWISE INFORMATIVE BADGE. B5.9 gave the review
     row SIGHT of the sequence and nothing else, on the reasoning that the
     queue is where a verdict is given rather than where a set is arranged.
     That held until a stray joining the wrong sequence turned out to be the
     common fault — and the queue is where it is SEEN. So one verb, the
     narrowest one: take this picture out.
     `pointer-events` BACK ON, for this and only this: the badge itself is
     `none` so it cannot swallow a click meant for the thumbnail under it. */
  .snip-pool-x { pointer-events: auto; margin: -2px -3px -2px 1px;
    padding: 0 3px; border: 0; border-radius: 3px; cursor: pointer;
    font: inherit; line-height: 1.35; color: var(--text-3);
    background: transparent; }
  .snip-pool-x:hover { color: var(--tl-not); background: rgba(255, 255, 255, 0.08); }
  .snip-pool-x:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
  .snip-play { position: absolute; inset: 0; display: grid; place-items: center;
    color: var(--text-2); opacity: 0.55; transition: opacity 0.15s ease; }
  .snip:hover .snip-play { opacity: 0; }

  .snip-title { font-family: var(--font-display); font-weight: 600; font-size: 0.95rem;
    line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden; }
  .snip-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }

  /* One rule, four colours, driven by data-kind. The kind IS the colour — a
     character reads as a character at a glance in a list of a thousand. */
  .tl { font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: 0.02em;
    padding: 3px 7px; border-radius: 6px; border: 0; cursor: pointer;
    background: color-mix(in srgb, var(--tl) 16%, transparent); color: var(--tl);
    transition: background 0.15s ease; }
  .tl:hover { background: color-mix(in srgb, var(--tl) 28%, transparent); }
  .tl[aria-pressed="true"] { background: var(--tl); color: var(--bg); }
  /* The ✕ only appears on hover, so a list of a thousand rows is not a wall
     of delete buttons — and it is PAINTED ON the chip now rather than tucked
     against it, which is the whole of S8.
     IT USED TO BE AN APPENDAGE: `display: none` at rest, `inline-flex` on
     hover. So the chip grew under the pointer and every chip after it on the
     line was pushed — 14.94px across 130 chips on a 34-tag row, measured, and
     on a wrapped row the last line re-wraps outright. Absolutely positioned,
     the chip's box is its name and its padding in every state and there is
     nothing for a hover to push.
     RED THE MOMENT IT ARRIVES, not only under the pointer. The veil does one
     thing, and saying so on sight means nobody has to hunt for which part of
     it is the button; `--tl-not` is the colour of removal everywhere else on
     this line, so it is already the word for this.
     NO POINTER AT REST, which is what keeps the chip's own click — the filter
     toggle — working across its full width. The veil takes the pointer only
     once it is visible, and that is the same moment. */
  .tl-wrap { display: inline-flex; align-items: stretch; position: relative;
    border-radius: 6px; overflow: hidden; }
  /* NEVER MORE THAN HALF THE CHIP, which is a guarantee and not a taste:
     the chip's own centre sits at half its width, so a veil wider than that
     covers the spot anybody aims at to FILTER by a tag and turns it into
     remove. `emote` is 40px and a flat 24px veil started at 16 — reported by
     `rvfind`, which stopped being able to press the chip at all. */
  .tl-x { position: absolute; top: 0; right: 0; bottom: 0;
    width: min(24px, 40%);
    border: 0; padding: 0; margin: 0; cursor: pointer;
    display: grid; place-items: center; line-height: 0;
    color: rgba(255, 255, 255, 0.82);
    background: linear-gradient(90deg, transparent,
      color-mix(in srgb, var(--tl-not) 86%, transparent) 38%);
    opacity: 0; pointer-events: none;
    transition: opacity 0.12s ease, background 0.12s ease, color 0.12s ease; }
  .tl-wrap:hover .tl-x { opacity: 1; pointer-events: auto; }
  /* A DRAWN ✕ AND NOT THE CHARACTER. A glyph's weight and its size are the
     font's business and move when the font does; two lines are two lines at
     whatever size they are asked for. */
  .tl-x svg { width: min(15px, 62%); height: auto; aspect-ratio: 1; display: block;
    transition: transform 0.12s ease; }
  /* AND IT ANSWERS THE POINTER ITSELF. The veil already says "remove"; this
     says "this press, now" — the red flat and solid, the glyph white and a
     touch larger. Three cheap things rather than one loud one. */
  .tl-x:hover, .tl-x:focus-visible { color: #fff;
    background: linear-gradient(90deg,
      color-mix(in srgb, var(--tl-not) 55%, transparent), var(--tl-not) 23%); }
  .tl-x:hover svg, .tl-x:focus-visible svg { transform: scale(1.18); }
  .tl-x:focus-visible { opacity: 1; outline: 1px solid #fff; outline-offset: -2px; }
  /* WHICH TAG AM I ABOUT TO LOSE — the question a wrapped row of forty raises,
     and the one a remove button on its own does not answer. `:has()` rather
     than `~` so the three renderers keep their markup order: the ✕ is written
     after the chip in all of them, and a sibling combinator only looks
     forward. */
  .tl-wrap:has(.tl-x:hover) .tl { text-decoration: line-through; }
  /* THE CHIP GEOMETRY, SAID OUT LOUD. This wore `.tl` for its shape and then
     spent an `!important` escaping `.tl`'s background and border — which is
     what `!important` nearly always is in a stylesheet: a rule fighting a
     parent it only wanted the size of. Those four declarations are copied
     down from `.tl` and there is nothing left to cancel. */
  /* 2px 6px, NOT the 3px 7px it is standing next to, and the difference is
     the border. `.tl` has `border: 0`; `.btn-add` brings `1px dashed`, so the
     same padding makes a box two pixels taller and two wider than every chip
     in the row — which on a line of 20px chips is a row that visibly grows
     when this button appears. Padding compensates for the line so the OUTER
     box matches: 2+1 = 3 down, 6+1 = 7 across.
     This is the thing to check for every member of the family: whatever it
     wore before `.btn-add` either had a border already (`.mu-tag`, `.cs-btn`,
     a fixed 22/26px square) or it did not, and only the ones that did not
     need the pixel back. */
  .tl-add { font-size: 0.64rem; letter-spacing: 0.02em;
    padding: 2px 6px; border-radius: 6px; }
  .tl-add:hover { background: var(--accent-soft); }

  /* ---- the filter line ----------------------------------------------------
     Count on the left, then the expression as chips. min-height so the list
     below does not jump a line the first time a filter appears. */
  /* Songs | Concerts. Two buttons and no select, because there are two of
     them and there will not be a third: a select hides one of two answers
     behind a click and tells you nothing about what the other one is. */
  /* ONE control, two names on the selector rather than two copies of the
     rule. Music's shelf and the Gallery's are the same object — a segmented
     pill that says which slice of a collection you are looking at — and the
     thing that must never drift is how they look. If they are ever given a
     shared name, these are renamed together. */
  /* ══ SEG ═══════════════════════════════════════════════════
     ONE OF N, and the choice is the whole control — which shelf, which
     collection. A pill holding its options, the chosen one lit.

     Three wear this: the gallery's shelf picker, the music one, and the
     upload bench's. The first two already shared every rule by being named
     together in each selector — which works, and is sharing by repetition:
     four selectors each listing both names, and a third control with
     nowhere to join. A base is the same arrangement with one name in it.

     WHAT IT COST, concretely: the bench's picker arrived wearing `.btn-op`
     with a lit rule of its own, in a different colour from the two controls
     doing the identical job twelve pixels away. Nothing was broken. It was
     simply a fourth answer to a question already answered three times.

     `aria-pressed` IS the state, read by the stylesheet directly. A class
     alongside it would be a second place for the same fact, and the second
     place is the one that goes stale. */
  .seg { display: inline-flex; gap: 2px; padding: 2px; border-radius: 999px;
    background: var(--glass-sunk); border: 1px solid var(--border); }
  .seg button { padding: 4px 13px; border: 0; border-radius: 999px;
    background: transparent; color: var(--text-3); font: inherit;
    font-size: 0.78rem; font-weight: 600; cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease; }
  .seg button:hover:not(:disabled) { color: var(--text-2); }
  /* A STATE THIS CONTROL CAN BE IN NOW. Nothing had ever disabled a segment
     before the bench locked its collection picker, so a locked pill looked
     exactly like an idle one and still wore the hand cursor — a control that
     is inert for a reason has to say so without being hovered. */
  .seg button:disabled { opacity: 0.45; cursor: not-allowed; }
  .seg button[aria-pressed="true"], .seg button[aria-checked="true"] {
    background: var(--accent-soft); color: var(--accent); }
  /* Four pills and one long label, which is a row that does not fit a phone.
     Music's two never needed this. */
  .gal-view { flex-wrap: wrap; }
  /* QUIET, NOT GONE, while the All switch is on. The shelf you were standing
     on is still lit and still says so — it is what you go back to when the
     switch goes off, and a picker that disappeared would take that answer
     with it. */
  /* ON THE SHELVES AND NOT ON THE TRACK, now that All lives in it: dimming
     the container took the switch down with the thing it was switching off,
     which drew a control reporting itself inert while it was the only one
     doing anything. */
  .gal-view.is-off button[data-galview] { opacity: 0.45; cursor: default; }
  /* "And also", in one pixel. A gap would have read as the end of the group
     and a second track as a second question. */
  .seg-rule { align-self: stretch; width: 1px; margin: 3px 4px;
              background: var(--border); }
  .seg .seg-all[aria-pressed="true"] { background: var(--accent);
                                       color: var(--on-accent); }
  /* NO MARKUP WEARS THESE ANY MORE. All moved into the shelf track and became
     a pill; the `.toggle gal-all` switch that stood beside the picker is gone
     from index.html. Kept rather than deleted because the sizing argument
     below is the answer to a question that comes back every time a `.toggle`
     is put next to a word shorter than `Pool` — and four dead rules cost less
     than working it out again.
     The switch is narrower than the bench's: `Pool` and `All` are the words,
     and a 92px track built for the longer one leaves this one swimming. */
  .gal-all input[type="checkbox"] { width: 72px; }
  .gal-all label::before { width: 72px; }
  .gal-all .toggle-word { width: 34px; }
  .gal-all input[type="checkbox"]:checked ~ label .toggle-word {
    transform: translate3d(34px, 0, 0); }

  .snip-meta-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
    margin: -10px 0 16px; min-height: 22px; }
  .snip-meta-row .snip-tags { margin-top: 0; }

  /* A filter chip says which of the three things it is doing, in front of the
     name, in the place the operator was typed. An excluded tag is struck
     through in the colour of removal rather than merely un-highlighted —
     "without asmr" and "not filtering on asmr" are different queries and must
     not look the same. */
  .tl-op { font-family: var(--font-mono); font-weight: 700; margin-right: 3px; opacity: 0.8; }
  .tl-f[data-mode="and"] { background: var(--tl); color: var(--bg); }
  .tl-f[data-mode="or"] { background: color-mix(in srgb, var(--tl) 34%, transparent);
    color: var(--tl); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tl) 60%, transparent); }
  .tl-f[data-mode="not"] { background: color-mix(in srgb, var(--tl-not) 20%, transparent);
    color: var(--tl-not); text-decoration: line-through;
    text-decoration-thickness: 1px; text-decoration-color: color-mix(in srgb, var(--tl-not) 70%, transparent); }
  .tl-wrap:has(.tl-f[data-mode="not"]) .tl-x { background: color-mix(in srgb, var(--tl-not) 20%, transparent);
    color: var(--tl-not); }

  /* A dotted underline on hover, so the second gesture is discoverable without
     decorating every segment permanently. */
  .tr-ed:hover { text-decoration: underline dotted var(--text-3) 1px;
    text-underline-offset: 3px; }
  .tr-in { width: 100%; font: inherit; color: var(--text); background: var(--surface);
    border: 1px solid var(--accent); border-radius: 6px; padding: 2px 6px; }

  .tl[data-kind="character"] { --tl: var(--tl-character); }
  .tl[data-kind="copyright"] { --tl: var(--seg-copyright); }
  .tl[data-kind="meta"]      { --tl: var(--tl-meta); }
  .tl[data-kind="artist"]    { --tl: var(--tl-artist); }
  .tl[data-kind="general"]   { --tl: var(--tl-general); }

  /* One flowing block, not a stack of rows. A two-minute clip is a paragraph
     of speech and reads like one; a row per line turns forty words into forty
     list items and makes the eye stop at every timestamp. The demarcations are
     still there — each segment is its own span, so it highlights on hover,
     seeks on click, and lights up as the clip reaches it — they just are not
     drawn as furniture. */
  .snip-tr { max-height: 118px; overflow-y: auto; overscroll-behavior: contain;
    padding-right: 4px; font-size: 0.79rem; line-height: 1.6; color: var(--text-2); }
  .snip-tr::-webkit-scrollbar { width: 6px; }
  .snip-tr::-webkit-scrollbar-thumb { background: var(--surface-3); border-radius: 3px; }
  .snip-tr-none { font-size: 0.75rem; color: var(--text-3); font-style: italic; }
  /* inline-block, and that is the whole bug fix. As a block it took the
     scroller's full width, then `margin: -3px -5px` widened it by ten pixels
     past that and `overflow-y: auto` answered with a horizontal scrollbar on
     every transcript-less row. Sized to its text, the negative margins pull
     the highlight outward without anything to overflow. */
  .snip-tr-add { display: inline-block; cursor: text; border-radius: 6px;
    padding: 3px 5px; margin: -3px -5px;
    transition: background 0.14s ease, color 0.14s ease; }
  .snip-tr-add:hover, .snip-tr-add:focus-visible {
    background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--text-2); }
  /* Negative margin against the padding so the highlight has breathing room
     without the words shifting when it appears. */
  .tr-w { padding: 1px 3px; margin: 0 -3px; border-radius: 4px; cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease; }
  .tr-w:hover { background: var(--surface-2); color: var(--text); }
  .tr-w.on { background: var(--accent-soft); color: var(--text); }
  .tr-sp { color: var(--tl-character); font-weight: 600; }

  /* THE WHOLE-TRANSCRIPT EDITOR used to live here — a pencil, sticky inside
     the scroller and floated so the words closed around it, revealed on
     hover. All of that is gone: it is an operation on the clip like the other
     five, so it is in `.snip-acts` with them and wears `.btn-ent`. What the
     old arrangement cost: the one control that rewrites every line of a
     transcript was the hardest to find on the card. */

  .snip-tr.tr-editing { max-height: none; overflow: visible; }
  /* Monospace and unwrapped-looking on purpose: the timestamps have to line up
     into a column the eye can run down, which is the whole reason this is text
     and not prose. pre-wrap rather than pre so a long segment stays readable
     instead of pushing a horizontal scrollbar into a panel that has one. */
  .tr-all { display: block; width: 100%; box-sizing: border-box; resize: vertical;
    min-height: 96px; white-space: pre-wrap; overflow-wrap: break-word;
    font-family: var(--font-mono); font-size: 0.76rem; line-height: 1.65;
    color: var(--text); background: var(--surface); border: 1px solid var(--accent);
    border-radius: 8px; padding: 8px 10px; }
  .tr-all:focus { outline: none; box-shadow: 0 0 0 2px var(--accent-soft); }
  .tr-all-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
    margin-top: 8px; }
  /* Clipped rather than wrapped: in a narrow bar — the expanded card, the
     phone layout — a second line of hint shoves the buttons down and moves
     Save out from under a pointer already on the way to it. The hint is the
     part that can afford to be cut. */
  .tr-all-hint { flex: 1 1 120px; min-width: 0; font-size: 0.68rem; color: var(--text-3);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .tr-all-hint b { font-family: var(--font-mono); font-weight: 600; color: var(--text-2); }
  /* The complaint takes the hint's place rather than appearing under it, so
     the bar does not grow a line and shift the buttons out from under the
     pointer at the moment somebody is reaching for them. */
  .tr-all-bar.bad .tr-all-hint { display: none; }
  .tr-all-err { flex: 1 1 200px; font-size: 0.7rem; color: var(--tl-not); }
  .tr-all-save { padding: 8px 13px; font-size: 0.8rem; }

  /* Editing takes the row's full width. Four hundred pixels turns every
     segment into two ragged lines and buries the timestamps — the things being
     edited — at the fold of each wrap. The media cell keeps its place and its
     size, so the clip goes on playing while the onsets are tweaked, which is
     the only way to tell whether an onset is right.
     After .snip.big deliberately: expanded, the transcript already has a tall
     column of its own beside the video, and that is the better shape for
     watching and typing at once. */
  .snip.tr-open { grid-template-areas: "media body body" "tr tr tr"; }
  .snip.tr-open > .snip-media { grid-area: media; }
  .snip.tr-open > .snip-body { grid-area: body; }
  .snip.tr-open > .snip-tr { grid-area: tr; }
  .snip.hasv.tr-open { grid-template-areas: "media body verdict" "tr tr tr"; }
  .snip.big.tr-open { grid-template-areas: "media body" "media tr"; }

  /* ---- expanded ----------------------------------------------------------
     Click the picture and the card gives the clip half its width, keeping the
     aspect ratio. Not a lightbox: the tags and the transcript stay beside it,
     because the point of watching it here rather than in a player is that the
     rest of the row is what tells you what you are looking at. */
  .snip.big { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: "media body" "media tr";
    grid-template-rows: auto 1fr; align-items: stretch; }
  /* Expanded, the cell takes the CLIP's ratio rather than 16:9. The list view
     crops deliberately — a grid of mixed shapes is unreadable — but cropping
     the thing you opened in order to look at it is the one place that trade
     stops being worth it: a 1:1 clip loses its top and bottom, and a 9:16
     phone clip is mostly gone. --ar is set per row from the stored width and
     height, so the box is correct before the video has loaded and nothing
     jumps; `contain` then covers the case where dimensions were never
     recorded and the fallback ratio is a guess. */
  /* Capped, because a 9:16 phone clip given half the card's width is over a
     thousand pixels tall and the transcript beside it ends up somewhere below
     the fold. The cap shortens the BOX; `contain` then puts bars at the sides
     rather than cropping, so the trade is a smaller picture and never a
     clipped one. */
  .snip.big .snip-media { grid-area: media; width: auto;
    aspect-ratio: var(--ar, 16 / 9); max-height: calc(var(--vh) * 62); }
  .snip.big .snip-frame img, .snip.big .snip-frame video { object-fit: contain; }
  .snip.big .snip-frame { background: #000; }
  .snip.big .snip-body { grid-area: body; }
  .snip.big .snip-tr { grid-area: tr; max-height: none; min-height: 0; }
  /* No hover scale once it is already big — growing a 700px video by another
     16% pushes it under the neighbouring card for no gain. */
  .snip.big:hover .snip-frame, .snip.big:focus-within .snip-frame {
    transform: none; box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45); }
  .snip.big .snip-play { opacity: 0; }
  .snip.big .snip-dur { display: none; }          /* the controls say it better */
  .snip-media { cursor: zoom-in; }
  /* Not zoom-out: once expanded the video owns its own clicks — play/pause and
     the scrubber are native — so shrinking gets its own control rather than
     stealing a gesture the player already means something by. */
  .snip.big .snip-media { cursor: default; }
  .snip-shrink { position: absolute; top: 6px; right: 6px; z-index: 4; display: none;
    width: 26px; height: 26px; padding: 0; border: 0; border-radius: 7px;
    background: rgba(10, 8, 14, 0.74); color: var(--text-2); cursor: pointer;
    align-items: center; justify-content: center;
    transition: background 0.15s ease, color 0.15s ease; }
  .snip.big .snip-shrink { display: flex; }
  .snip-shrink:hover { background: rgba(10, 8, 14, 0.92); color: var(--text); }

  .snips-empty { padding: 28px; text-align: center; color: var(--text-3); }
  /* THE NUMBER IN A COUNT LABEL, WHICH IS NOW A BUTTON. Styled as the text it
     sits in rather than as a control: it is still the number, and the dotted
     underline is the only thing saying it can be pressed. A real button here
     would read as a second action beside the ones in the toolbar. */
  .cnt-ask { font: inherit; color: inherit; background: none; border: 0;
             padding: 0; cursor: pointer; text-decoration: underline dotted;
             text-underline-offset: 2px; }
  .cnt-ask:hover { color: var(--text-2); }
  .cnt-ask:disabled { cursor: default; text-decoration: none; opacity: .6; }

  /* A room that is not built yet. Deliberately quiet and deliberately not a
     spinner or a skeleton: nothing is loading and nothing is coming in a
     moment, and animating either would say otherwise. */
  /* ── the tools panel ────────────────────────────────────────────────────
     A sidebar and a body. The sidebar is the only navigation, so it keeps its
     width and the body takes the rest; at phone width it becomes a row of
     chips above the body rather than a column stealing half the screen. */
  .tp-wrap { display: grid; grid-template-columns: 208px minmax(0, 1fr);
    gap: 18px; align-items: start; padding: 4px 0 40px; }
  .tp-side { display: flex; flex-direction: column; gap: 2px; position: sticky;
    top: 8px; }
  .tp-pick { display: flex; align-items: center; gap: 9px; text-align: left;
    padding: 9px 10px; border: 0; border-radius: 9px; background: transparent;
    color: var(--text-2); font: inherit; font-size: 0.82rem; cursor: pointer; }
  .tp-pick svg { width: 17px; height: 17px; flex: none; opacity: 0.8; }
  .tp-pick:hover { background: var(--surface-2); color: var(--text); }
  .tp-pick[aria-selected="true"] { background: var(--accent-soft); color: var(--text);
    box-shadow: inset 2px 0 0 var(--accent); }
  .tp-pick[aria-selected="true"] svg { opacity: 1; color: var(--accent); }
  .tp-body { min-width: 0; }
  .tp-head { margin: 2px 0 16px; }
  .tp-head h2 { margin: 0 0 4px; font-family: var(--font-display); font-size: 1.06rem;
    font-weight: 700; letter-spacing: -0.01em; }
  .tp-head p { margin: 0; font-size: 0.8rem; line-height: 1.6; color: var(--text-3);
    max-width: 55ch; }
  /* Two columns where there is room: the form on the left, the preview on the
     right, so a change to a clock is visible in the cue times without
     scrolling between them. */
  .tp-cols { display: grid; grid-template-columns: minmax(0, 380px) minmax(0, 1fr);
    gap: 18px; align-items: start; }
  .tp-fld { display: block; margin-bottom: 12px; }
  .tp-fld > span { display: block; margin-bottom: 5px; font-family: var(--font-mono);
    font-size: 0.56rem; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--text-3); }
  .tp-fld select, .tp-fld input, .tp-fld textarea { width: 100%; box-sizing: border-box;
    background: var(--surface-2); color: var(--text); border: 1px solid var(--surface-3);
    border-radius: 8px; padding: 8px 10px; font-family: inherit; font-size: 0.8rem; }
  .tp-fld textarea { min-height: 168px; resize: vertical; line-height: 1.55;
    font-family: var(--font-mono); font-size: 0.72rem; }
  .tp-fld select:focus, .tp-fld input:focus, .tp-fld textarea:focus {
    outline: none; border-color: var(--accent); }
  .tp-hint { margin: -7px 0 12px; font-size: 0.7rem; line-height: 1.55; color: var(--text-3); }
  /* ── the hierarchy ──────────────────────────────────────────────────────
     A tree of rows, indented inline, because depth is the only thing the
     layout has to say and a class per level is six rules that have to agree. */
  .hx-bar { display: flex; align-items: center; gap: 10px; margin: 0 0 10px; }
  .hx-bar input { width: 100%; box-sizing: border-box; font: inherit; font-size: 0.82rem;
    height: 34px; padding: 0 10px; border-radius: 8px; color: var(--text);
    background: var(--glass-sunk); border: 1px solid var(--border); }
  .hx-bar input:focus { border-color: var(--accent); outline: none; }
  /* CLEAR, and deliberately not a bordered button. It empties a working set
     and deletes nothing -- the title says so -- so it reads as a word you can
     press rather than as a control with consequences. The height is stated
     rather than inherited so it cannot drift from the box beside it, which is
     the whole of what made this bar look like three rows stacked. */
  /* The height is the point here and the only thing left: this sits beside a
     34px box and matching it is what stopped the bar reading as three rows
     stacked. The four typographic declarations that used to follow were
     `.btn-op` spelled out a second time, four thousand lines from the first. */
  .hx-bar .hx-clear { height: 34px; display: inline-flex; align-items: center;
    padding: 0 12px; }
  /* THE COUNT, under the subtitle rather than in the bar. In the bar it was a
     third item competing with the two controls, and it is not a control -- it
     is a fact about the vocabulary, which is what the head is for. */
  .hx-count { margin: -8px 0 14px; font-family: var(--font-mono); font-size: 0.66rem;
    letter-spacing: 0.04em; color: var(--text-3); }
  /* DOWN TO THE BOTTOM OF THE PAGE. This was `calc(var(--vh) * 62)` — about
     two thirds — so the tree ended in mid-air with the rest of the panel
     empty beneath it, on every screen. The box starts 292px down and that
     number does not move with the viewport: the header, the tab row, the
     tool's own title and its search bar are all fixed heights. Subtracting
     320 finishes it a few pixels above the fold.
     `max()` rather than a bare calc so a short window gets a usable box
     rather than a sliver. */
  .hx-tree { max-height: max(220px, calc(var(--vh) * 100 - 320px)); overflow-y: auto;
    padding: 2px; border-radius: 10px; border: 1px solid var(--border);
    background: var(--glass-sunk); }
  .hx-row { display: flex; align-items: center; gap: 8px; padding: 3px 8px 3px 0;
    border: 1px solid transparent; border-radius: 6px; }
  .hx-row:hover { background: rgba(255,255,255,0.03); }
  /* An ancestor drawn only to say where a tag sits. Dimmed so the branch reads
     as a path rather than a list of equals; it keeps every control, because
     correcting a parent two levels up is a thing you come here to do. */
  .hx-thin { opacity: 0.62; }
  .hx-thin:hover { opacity: 1; }
  .hx-find { position: relative; flex: 1 1 auto; min-width: 0; }
  .hx-find .snip-ac { top: calc(100% + 3px); left: 0; right: 0; }
  .hx-tw { width: 16px; flex: none; padding: 0; border: 0; background: transparent;
    color: var(--text-3); cursor: pointer; font-size: 0.7rem; line-height: 1; }
  .hx-tw:disabled { cursor: default; opacity: 0; }
  /* THE NAME IS THE ROW, so it is drawn as a thing and not as text lying on
     the tree's own background. That background is `--glass-sunk`,
     `rgba(0,0,0,0.24)` over whatever is behind it, and `--text-2` on top of
     it is thin at night and thinner in the day room, where the page is
     `#59525a` and the contrast goes with it. A surface the rooms share, plus
     the full ink, reads in both. */
  .hx-n { font-family: var(--font-mono); font-size: 0.76rem; color: var(--text);
    background: var(--surface-2); border: 1px solid var(--border);
    padding: 2px 8px; border-radius: 6px; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* `margin-left: auto` WAS HERE, and it pushed the count — and therefore the
     controls after it — to the far right of a panel that is most of the
     screen wide. `+ child` ended up several inches from the name it is about,
     with nothing in between. Everything packs left now and the slack falls
     off the end of the row, which is where nothing needs it. */
  .hx-c { font-family: var(--font-mono); font-size: 0.66rem;
    color: var(--text-3); flex: none; }
  /* THE TWO IN THIS ROW THAT ADD SOMETHING. `+ parent` and `+ child` put a
     tag into the tree; the two beside them (back-fill, lift out) act on one
     that is already in it, so they are operators. A row with two dashed
     outlines and two bare glyphs is the point rather than an inconsistency
     — it says which two make something that is not there yet.

     THE GEOMETRY IS HERE because `.btn-add` has none. Its members are a
     22px square, a 26px pill, a chapter pill and a whole card, so a default
     size on the base would be a size five of six have to cancel — which is
     the thing this family was untangled to stop. A member brings its own. */
  .hx-add { flex: none; padding: 4px 9px; border-radius: 7px;
    font-size: 0.7rem; }
  /* THE TWO ICON OPERATORS IN THIS ROW. Both are drawn now, so the size
     comes from `.btn-op .ico` and the only thing left that is local to this
     row is the box: 4px of vertical padding around the 15px icon puts them
     at 23px, which is the `+ child` button beside them exactly — the whole
     acts row is now one height.
     `op-danger` on the ✕ is the archive's removal colour rather than a
     warning of loss: the tag itself is untouched, which its own title says,
     and `--tl-not` is the same red an excluded filter chip wears. It marks
     the act as a taking-away, not as a danger. */
  .hx-re, .hx-free { flex: none; padding: 4px 6px; }
  /* The row's controls, revealed on hover. In the flow rather than floated
     over the row: the tree's background is translucent in both themes, so an
     overlay would need an opaque colour neither of them has. The cost is that
     the name gives up room while the pointer is on the row — which is the one
     moment nobody is reading it.
     `focus-within`, with the ROW ITSELF focusable, because `display: none`
     cannot be tabbed into: without a focusable row these buttons would exist
     for the pointer only, and a leaf row has nothing else to focus — its
     twisty is disabled. */
  .hx-acts { display: none; align-items: center; gap: 6px; flex: none; }
  .hx-row:hover .hx-acts, .hx-row:focus-within .hx-acts { display: flex; }
  /* A focusable row needs to say where the focus IS. `focus-visible` and not
     `focus`, or a row would keep a ring after being clicked. */
  .hx-row:focus { outline: none; }
  .hx-row:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
  /* Where a drag would land, said while the pointer is there — the row under
     it, or the tree itself past the last row, which lifts to the root. A drop
     that would make a ring never gets the class, so the absence of this is
     the refusal. Written against `.hx-row` so it outranks the hover rule. */
  .hx-row.hx-over, .hx-tree.hx-over {
    border-color: var(--accent); background: var(--accent-soft); }

  /* ── the alias manager ──────────────────────────────────────────────────
     One row to add and a scrollable list under it. `grid` rather than flex so
     the handle and the artist columns line up with the rows below them —
     which is the whole reason a person can read the list as a continuation of
     the form rather than as a second thing. */
  /* THE TAG COMES FIRST, because it is the row the alias is being added to —
     the add line reads in the same order the list does. Five controls, which
     is one more than the four this grid held when the panel was two tables:
     the category used to be implied by which of them you were typing in. */
  .al-add { display: grid; gap: 8px; align-items: center; margin: 0 0 10px;
    grid-template-columns: minmax(0, 1.1fr) auto auto minmax(0, 1fr) auto; }
  /* 34px EXPLICITLY, on all of them. `.al-sitebtn` and `.al-pick` already said
     it and the input and the button were left to their own intrinsic heights,
     which is three numbers that happen to be close and drift the moment a
     font size moves. */
  .al-add input { font: inherit; font-size: 0.82rem; width: 100%; box-sizing: border-box;
    height: 34px; padding: 0 10px; border-radius: 8px; color: var(--text);
    background: var(--glass-sunk); border: 1px solid var(--border); }
  .al-add input:focus { border-color: var(--accent); outline: none; }
  /* `inline-flex` is what makes the height centre its own text: `.btn-primary`
     is a block with 12px of vertical padding, so a fixed height alone leaves
     the label sitting low in it. */
  .al-add .btn-primary { white-space: nowrap; height: 34px; padding: 0 14px;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 0.82rem; }
  /* THE SITE IS AN ICON, because the name is long and the glyph is the thing
     anybody actually recognises. The full name lives in the title, the aria
     label and the open list. */
  .al-site { position: relative; }
  /* WIDE ENOUGH FOR A NAME, not only for a glyph. One site has an icon and
     the other seven do not, and a `?` in a 38px square said nothing at all —
     which did not matter while X was the only source and does now. The glyph
     still gets the square; a name takes the room it needs. */
  .al-sitebtn { display: grid; place-items: center; min-width: 38px; height: 34px;
    padding: 0 9px; border-radius: 8px; cursor: pointer; color: var(--text-2);
    background: var(--glass-sunk); border: 1px solid var(--border); }
  .al-sitebtn:hover { color: var(--text); }
  .al-sitebtn svg { width: 15px; height: 15px; display: block; }
  .al-noicon { font-family: var(--font-mono); font-size: 0.72rem;
    white-space: nowrap; }
  /* The gap a mark would have filled, so the labels in the open list share
     one left edge whether or not their site has one. */
  .al-nomark { display: block; width: 15px; height: 15px; }
  .al-sites { position: absolute; z-index: 40; top: calc(100% + 4px); left: 0;
    min-width: 210px; padding: 4px; border-radius: 10px; background: var(--panel);
    border: 1px solid var(--border); box-shadow: 0 10px 26px rgba(0,0,0,0.45); }
  .al-sites button { display: flex; align-items: center; gap: 9px; width: 100%;
    padding: 7px 9px; border: 0; border-radius: 7px; background: transparent;
    color: var(--text-2); font: inherit; font-size: 0.82rem; cursor: pointer; text-align: left; }
  .al-sites button:hover { background: var(--surface-2); color: var(--text); }
  .al-sites button[aria-selected="true"] { color: var(--accent); }
  .al-sites svg { width: 14px; height: 14px; flex: none; }
  /* THE TOOL'S TITLE IS THE SAME TITLE EVERYWHERE, reverted here on purpose.
     This panel had its own: mono, upper-cased, letter-spaced, on the argument
     that a head over two tables of rules is a label rather than a sentence.
     Read beside the other four tools it was simply a fifth style, and the
     thing it was distinguishing was not worth a reader having to notice. So
     `.tp-head h2` is the one title and `.al-sub` is the section title under
     it -- which is where the mono-caps treatment actually belonged. */

  /* ── one list, where there were two ─────────────────────────────────────
     A heading each stood here, over a handles table and a content table, and
     the distinction they drew is real — who DREW it, and what it is OF — and
     it is a CATEGORY, not a screen. `artist` is one of five, so the two lists
     are one list and the headings went with them. `.al-sub` lives on for the
     record editor, defined with `.rec-sec` up there. */
  /* Shaped like the field beside it and pressed like a button, because what it
     holds is a ROW and not a string — the picker is the only way to name one,
     here as everywhere else in the archive. Both tables use it. */
  .al-pick { display: flex; align-items: center; min-width: 0; height: 34px;
    padding: 0 10px; border-radius: 8px; cursor: pointer; font: inherit;
    font-size: 0.78rem; color: var(--text-3); text-align: left;
    background: var(--glass-sunk); border: 1px solid var(--border); }
  .al-pick:hover { border-color: var(--surface-3); color: var(--text-2); }
  .al-pick .tl { pointer-events: none; }

  /* Its own container and scrollable: a vocabulary of accounts grows without
     bound and a screen that grows with it stops having a shape. */
  .al-list { max-height: calc(var(--vh) * 46); overflow-y: auto; padding: 2px;
    border-radius: 10px; border: 1px solid var(--border); background: var(--glass-sunk); }
  /* ── one tag and its names ──────────────────────────────────────────────
     Two columns and not four: the tag, and a bag of keys. The keys WRAP,
     which is the whole reason this is not a table — a character with one
     alias and a character with nine both belong in this list, and a grid with
     a column per site would be mostly empty.
     The tag column is wide and fixed, so the keys of every row start at the
     same x and the list scans down. */
  .ag-row { display: grid; gap: 10px; align-items: baseline; padding: 7px 8px;
    grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
    border-bottom: 1px solid rgba(255,255,255,0.05); }
  .ag-row:last-child { border-bottom: 0; }
  .ag-t { min-width: 0; }
  .ag-keys { display: flex; flex-wrap: wrap; gap: 5px; align-items: center;
    min-width: 0; }
  /* One key: the qualifier, the word, and the way out. The group is the base
     and what sits inside it are members, so the face is said once here. */
  .agk { display: inline-flex; align-items: stretch; flex: none;
    border-radius: var(--radius-sm); overflow: hidden;
    border: 1px solid var(--border); background: var(--glass-sunk);
    font-family: var(--font-mono); font-size: 0.7rem; }
  /* `danbooru.character:` — quieter than the word, because it is the part you
     scan past once you have found the row you wanted. */
  .agk-pre { padding: 2px 0 2px 7px; color: var(--text-3); align-self: center;
    border: 1px solid transparent; border-radius: 4px; cursor: pointer; }
  .agk-pre:hover, .agk-pre:focus { border-color: var(--surface-3); outline: none;
    color: var(--text-2); }
  .agk-key { padding: 2px 6px 2px 0; color: var(--text); align-self: center;
    border: 1px solid transparent; border-radius: 4px; cursor: text; }
  .agk-key:hover, .agk-key:focus { border-color: var(--surface-3); outline: none; }
  .agk-x { flex: none; border: 0; border-left: 1px solid var(--border);
    background: none; font: inherit; font-size: 0.68rem; line-height: 1;
    padding: 2px 6px; color: var(--text-3); cursor: pointer; }
  .agk-x:hover { background: color-mix(in srgb, var(--tl-not) 22%, transparent);
    color: var(--text); }
  /* The row's own adder, which is this shape's point: you are looking at a tag
     that is missing a site. Sized to sit in the run of keys. */
  .ag-plus { flex: none; width: 22px; height: 22px; border-radius: var(--radius-sm);
    display: grid; place-items: center; font-size: 0.8rem; line-height: 1; }
  .ag-plus:hover { border-color: var(--accent); color: var(--accent); }
  .al-bar { display: flex; align-items: center; gap: 10px; margin: 0 0 9px; }
  .al-bar input { flex: 0 1 340px; min-width: 0; height: 34px; padding: 0 10px;
    border-radius: 8px; font: inherit; font-size: 0.78rem; color: var(--text);
    background: var(--glass-sunk); border: 1px solid var(--border); }
  .al-bar .al-n { flex: none; font-size: 0.68rem; color: var(--text-3); }
  /* The category, shaped like the field beside it. */
  .al-ns { height: 34px; padding: 0 8px; border-radius: 8px; font: inherit;
    font-size: 0.76rem; font-family: var(--font-mono); color: var(--text-2);
    background: var(--glass-sunk); border: 1px solid var(--border); cursor: pointer; }
  @media (max-width: 720px) {
    /* The tag over its keys rather than beside them: at this width a 15rem
       column leaves the keys nowhere to wrap into, and the add line is five
       controls that cannot share one row either. */
    .ag-row { grid-template-columns: minmax(0, 1fr); gap: 5px; }
    .al-add { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
    .al-bar input { flex: 1 1 200px; }
  }

  /* ── the blacklist ──────────────────────────────────────────────────────
     One row per word this archive has decided means nothing here. It reuses
     `.al-list` for the box because it IS that box — a scrollable list of
     standing rules — and defines only its own row, which is three columns
     rather than four: there is no tag at the far end, which is the whole
     point of the list.
     THE ORIGIN IS SHOWN AS A WHOLE, `danbooru.meta`, because that is the key.
     Either half alone is ambiguous: `highres` is a word, `danbooru` is a
     dozen namespaces, and a reader deciding whether to put something back
     needs the pair. */
  /* A FLOOR UNDER THE ORIGIN COLUMN and not a fixed width: the chip keeps its
     own size, and the words start at one x so the list scans down the thing
     you are looking for. `danbooru.copyright` is the longest of the five that
     matter; anything longer takes the column with it. */
  .bl-row { display: grid; gap: 10px; align-items: center; padding: 5px 8px;
    grid-template-columns: minmax(142px, auto) minmax(0, 1fr) auto auto;
    border-bottom: 1px solid rgba(255,255,255,0.05); }
  .bl-row:last-child { border-bottom: 0; }
  .bl-org { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-3);
    padding: 2px 6px; border-radius: var(--radius-sm); background: var(--glass-sunk);
    border: 1px solid var(--border); white-space: nowrap; }
  .bl-key { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .bl-who { font-size: 0.66rem; color: var(--text-3); white-space: nowrap; }
  .bl-bar { display: flex; align-items: center; gap: 10px; margin: 0 0 9px; }
  /* The list's own empty line, which `.al-list` has no padding for: its rows
     carry their own, so a bare hint sits on the border. */
  .al-list > .tp-hint { padding: 9px 10px; margin: 0; }
  .bl-bar input { flex: 0 1 340px; min-width: 0; height: 34px; padding: 0 10px;
    border-radius: 8px; font: inherit; font-size: 0.78rem; color: var(--text);
    background: var(--glass-sunk); border: 1px solid var(--border); }
  .bl-bar .bl-n { flex: none; font-size: 0.68rem; color: var(--text-3); }
  @media (max-width: 720px) {
    /* The who/when column is the first thing to go: it is provenance, and the
       two that decide anything are the origin and the word. */
    .bl-row { grid-template-columns: minmax(0, auto) minmax(0, 1fr) auto; }
    .bl-bar input { flex: 1 1 200px; }
    .bl-row .bl-who { display: none; }
  }

  /* One row of the alias table: who, where, and the way out. */
  .tp-row { display: flex; align-items: center; gap: 8px; padding: 5px 2px;
    border-bottom: 1px solid rgba(255,255,255,0.05); }
  .tp-row:last-child { border-bottom: 0; }
  .tp-row .tp-mono { margin-left: auto; font-family: var(--font-mono);
    font-size: 0.68rem; color: var(--text-3); }
  .tp-row button.cs-btn { flex: none; }
  .tp-hint code { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-2); }
  .tp-row { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; }
  .tp-row .tp-fld { flex: 1 1 120px; margin-bottom: 12px; }
  .tp-chk { display: flex; align-items: center; gap: 8px; margin: -4px 0 8px;
    font-size: 0.79rem; color: var(--text-2); cursor: pointer; }
  .tp-chk input { width: 15px; height: 15px; accent-color: var(--accent);
    flex: none; cursor: pointer; }
  .tp-acts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 2px; }
  /* The preview. A table because the whole question is "did the right second
     come out", and a column of times is the only way to see that at a glance. */
  .tp-prev { border: 1px solid var(--surface-3); border-radius: 10px;
    background: var(--panel); overflow: hidden; }
  .tp-prev-h { display: flex; align-items: center; gap: 8px; padding: 8px 11px;
    border-bottom: 1px solid var(--surface-3); font-size: 0.73rem; color: var(--text-2); }
  .tp-prev-h b { color: var(--text); font-weight: 650; }
  .tp-prev-h span { flex: 1; }
  .tp-cue { display: flex; gap: 10px; padding: 5px 11px; font-size: 0.76rem;
    border-top: 1px solid color-mix(in srgb, var(--surface-3) 45%, transparent); }
  .tp-cue:first-of-type { border-top: 0; }
  .tp-cue-t { font-family: var(--font-mono); font-size: 0.7rem; color: var(--accent);
    flex: none; font-variant-numeric: tabular-nums; }
  .tp-cue-x { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; color: var(--text-2); }
  .tp-scroll { max-height: calc(var(--vh) * 52); overflow-y: auto; }
  /* What did NOT become a cue, and why. Never silent: a track shorter than the
     notes it came from is a thing you discover at the wrong moment. */
  .tp-skip { border-top: 1px solid var(--surface-3); padding: 8px 11px;
    font-size: 0.71rem; color: var(--tl-not); }
  .tp-skip b { display: block; margin-bottom: 3px; color: var(--text-2); font-weight: 600; }
  .tp-skip div { line-height: 1.5; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  .tp-empty { padding: 26px 12px; text-align: center; font-size: 0.79rem;
    color: var(--text-3); }
  @media (max-width: 900px) {
    .tp-wrap { grid-template-columns: 1fr; }
    .tp-side { position: static; flex-direction: row; flex-wrap: wrap; }
    .tp-cols { grid-template-columns: 1fr; }
  }

  .panel-soon { max-width: 460px; margin: calc(var(--vh) * 12) auto; text-align: center;
    display: flex; flex-direction: column; align-items: center; gap: 10px; }
  .panel-soon .ps-ico { display: grid; place-items: center; width: 54px; height: 54px;
    border-radius: 16px; background: var(--surface-2); color: var(--text-3); }
  .panel-soon .ps-ico svg { width: 24px; height: 24px; }
  .panel-soon b { font-size: 0.95rem; font-weight: 650; color: var(--text-2); }
  .panel-soon span:not(.ps-ico) { font-size: 0.8rem; line-height: 1.6;
    color: var(--text-3); }

  /* ---- the vocabulary queue ----------------------------------------------
     One row per NAME, not per clip. Wider than it is tall and read top to
     bottom at speed: the name, how many people asked, and the decision. */
  .sg-list { display: flex; flex-direction: column; gap: 8px; }
  .sg { display: grid; grid-template-columns: 1fr auto; gap: 6px 14px;
    align-items: start; padding: 12px 14px; border-radius: 12px;
    border: 1px solid var(--border); background: var(--surface); }
  .sg-name { font-weight: 650; font-size: 0.95rem; color: var(--text); }
  .sg-n { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-3);
    white-space: nowrap; padding-top: 3px; }
  .sg-sub { grid-column: 1 / -1; color: var(--text-3); font-size: 0.72rem;
    display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; }
  .sg-sub em { font-style: normal; font-family: var(--font-mono); font-size: 0.68rem; }
  /* The clips that asked. Collapsed, because the decision is usually obvious
     from the name and the count — and when it is not, the titles are the
     context that settles it. */
  .sg-clips { grid-column: 1 / -1; margin: 2px 0 0; padding: 8px 10px;
    border-radius: 8px; background: var(--glass-sunk); font-size: 0.74rem;
    color: var(--text-2); display: flex; flex-direction: column; gap: 3px; }
  .sg-clips a { color: var(--text-2); text-decoration: none; }
  .sg-clips a:hover { color: var(--text); text-decoration: underline; }
  .sg-acts { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px;
    align-items: center; margin-top: 4px; }
  .sg-acts .lbl { color: var(--text-3); font-size: 0.72rem; margin-right: 2px; }
  .sg-kind { display: inline-flex; align-items: center; gap: 5px;
    padding: 4px 9px; border-radius: 7px; border: 1px solid var(--border);
    background: var(--surface-2); color: var(--text-2); font: inherit;
    font-size: 0.74rem; cursor: pointer; }
  .sg-kind:hover { background: var(--surface-3); color: var(--text); }
  .sg-kind i { width: 7px; height: 7px; border-radius: 2px; }
  /* An existing taglet with this exact slug. The one case where minting would
     be a mistake, so it takes the primary and pushes the kinds out of sight. */
  .sg-have { display: inline-flex; align-items: center; gap: 6px; padding: 4px 11px;
    border-radius: 7px; border: 1px solid transparent; background: var(--accent);
    color: var(--on-accent); font: inherit; font-size: 0.74rem; font-weight: 600;
    cursor: pointer; }
  .sg-drop { margin-left: auto; padding: 4px 9px; border-radius: 7px;
    border: 1px solid var(--border); background: none; color: var(--text-3);
    font: inherit; font-size: 0.74rem; cursor: pointer; }
  .sg-drop:hover { color: var(--tl-not); border-color: var(--tl-not); }
  .sg-more { background: none; border: 0; padding: 0; color: var(--text-3);
    font: inherit; font-size: 0.72rem; cursor: pointer; text-decoration: underline; }
  .sg-more:hover { color: var(--text-2); }
  .sg.busy { opacity: 0.5; pointer-events: none; }

  /* ---- you arrived here from a link --------------------------------------
     Somebody following a shared moment has no idea they are looking at one
     card out of a thousand, and the list gives them no clue: it looks exactly
     like an archive with one clip in it. So say so, and put the way out in
     the same sentence. */
  .moment-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    margin: -6px 0 16px; padding: 9px 14px; border-radius: 12px;
    border: 1px solid var(--glass-line); background: var(--glass-2);
    /* Same correction as .ops-ac: the blur() wrapper made this invalid, and
       --glass-2 is a 4.5% white wash — without the blur there is no glass
       here at all, just a very faint box. */
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur); color: var(--text-2);
    font-size: 0.82rem; }
  .moment-bar b { color: var(--text); font-weight: 600; }
  .moment-bar button { margin-left: auto; padding: 5px 11px; border-radius: 8px;
    border: 1px solid var(--glass-line); background: var(--glass-3);
    color: var(--text); font: inherit; font-size: 0.78rem; cursor: pointer; }
  .moment-bar button:hover { background: var(--glass-hi); }

  /* ---- which clock a typed timestamp is on -------------------------------
     Beside the add-note field, always visible rather than a setting you have
     to go and find. A note's time is only meaningful against a clock, so the
     clock belongs next to where the time is typed. */
  .nt-cp { display: inline-flex; align-items: center; gap: 5px; flex: 0 0 auto;
    padding: 3px 7px; border: 1px solid var(--border); border-radius: 7px;
    background: none; color: var(--text-3); font: inherit; font-size: 0.68rem;
    cursor: pointer; white-space: nowrap;
    transition: color 0.14s ease, border-color 0.14s ease; }
  .nt-cp:hover { color: var(--text); border-color: var(--text-3); }
  .nt-cp b { font-size: 0.6rem; opacity: 0.7; }
  /* Wraps, and the hints below take a whole row each. They are flex items on
     this line: with one short hint that looked fine, and the moment a second
     one appeared the three of them shared the width and the input you type
     into became forty pixels wide. */
  .ob-new, .nt-new { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
  /* basis 0, not auto. These inputs also carry width:100% further down, which
     as a flex-basis means "give me the whole row" — harmless while the row
     could not wrap, and the moment it could it pushed the clock chip onto a
     line of its own. */
  .ob-new textarea, .nt-new textarea { flex: 1 1 0; min-width: 0;
    /* Grown by hand in autoGrow(); the scrollbar would otherwise appear for the
       one frame between a keystroke and the measurement, and the drag handle is
       a second way to set a height that nothing reads. */
    resize: none; overflow: hidden; line-height: 1.45; }
  .ob-new .arm-hint, .nt-new .arm-hint,
  .ob-new .wall-hint, .nt-new .wall-hint { flex: 1 0 100%; margin-top: 0; }
  /* The chip sits against the top of a field that may now be several lines
     tall, rather than floating in the middle of it. */
  .ob-new, .nt-new { align-items: flex-start; }
  .ob-new .nt-cp, .nt-new .nt-cp { margin-top: 3px; }

  /* ---- review: which pile ------------------------------------------------ */
  /* ---- music --------------------------------------------------------------
     A grid of covers, because that is what these are: two-minute songs whose
     art is how anybody recognises one. The add tile is the Index's `vod-add`
     verbatim, sized to the card beside it. */
  .mu-grid { display: grid; gap: 14px;
    grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); }
  .mu { display: flex; flex-direction: column; border-radius: var(--radius);
    border: 1px solid var(--border); background: var(--surface); overflow: hidden; }
  .mu.busy { opacity: 0.5; pointer-events: none; }
  /* The clip row's two unsettled states, said the same way on a card. Same
     colours, same dashes, same reason: the Music panel has one list now, and a
     song waiting for a verdict has to be distinguishable from the shelf at a
     glance rather than by reading a chip. */
  .mu.is-pending { border-style: dashed;
    border-color: color-mix(in srgb, var(--tl-meta) 42%, transparent);
    background: color-mix(in srgb, var(--tl-meta) 5%, var(--surface)); }
  .mu.is-pending .mu-art { opacity: 0.78; }
  .mu.is-removed { border-style: dashed;
    border-color: color-mix(in srgb, var(--tl-not) 45%, transparent);
    background: color-mix(in srgb, var(--tl-not) 6%, var(--surface)); }
  .mu.is-removed .mu-art { opacity: 0.5; }
  .mu-going { opacity: 0; transform: scale(0.97);
    transition: opacity 0.2s ease, transform 0.2s ease; }
  /* 16:9 and cropped. YouTube's hqdefault is 4:3 with black bars baked in on
     older uploads, so the cover is cropped to the shape every card shares
     rather than letterboxed into it. */
  .mu-art { position: relative; display: block; width: 100%; aspect-ratio: 16 / 9;
    border: 0; padding: 0; cursor: pointer; background: var(--glass-sunk); }
  .mu-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
  /* No cover. Either YouTube has nothing for it any more, or the network is
     between us and them — and the card should read as "no picture" rather than
     as a layout that fell over. */
  .mu-art.mu-noart { display: grid; place-items: center; }
  .mu-art.mu-noart::before { content: 'no cover'; color: var(--text-3);
    font-family: var(--font-mono); font-size: 0.68rem; }
  .mu-art::after { content: ''; position: absolute; inset: 0; opacity: 0;
    background: rgba(0,0,0,0.35); transition: opacity 0.15s ease; }
  .mu-art:hover::after, .mu-art:focus-visible::after { opacity: 1; }
  .mu-play { position: absolute; inset: 0; display: grid; place-items: center;
    color: #fff; opacity: 0; transition: opacity 0.15s ease; z-index: 1; }
  .mu-art:hover .mu-play, .mu-art:focus-visible .mu-play { opacity: 1; }
  .mu-dur { position: absolute; right: 6px; bottom: 6px; z-index: 1;
    padding: 1px 5px; border-radius: 4px; background: rgba(0,0,0,0.72);
    color: #fff; font-family: var(--font-mono); font-size: 0.66rem; }
  /* Status lives on the cover, not in the body, and that is what keeps the
     grid regular. In the body it was a row that existed on some cards and not
     others — and because grid items stretch to their row, ONE card carrying a
     transient `saving…` made its whole row taller than the rows beneath it.
     Up here it costs no height at all, and it is beside the duration and the
     archive badge, which are the other two things the cover already says. */
  .mu-state { position: absolute; left: 6px; bottom: 6px; right: 46px; z-index: 1;
    display: flex; flex-wrap: wrap-reverse; gap: 4px; }
  .mu-state .mu-chip { background: rgba(0,0,0,0.72); border-color: rgba(255,255,255,0.14);
    color: #e8e6ec; backdrop-filter: blur(3px); }
  .mu-state .mu-chip.bad { color: #ff9ba0; border-color: rgba(255,155,160,0.4); }
  /* ---- one card, one height ------------------------------------------------
     Every row below RESERVES its space whether or not it has anything in it,
     the way the index's VOD title does. A grid where a card with two tag rows
     is taller than the one beside it does not read as denser — it reads as
     broken, and the eye spends its first moment on the ragged edge instead of
     on the covers. The cost is some empty space on the sparsest card, which is
     the right trade at three or four across.

     What is NOT reserved: the status chips and the action row. Both are absent
     for everybody or present for everybody within a pile — a shelf of
     published songs has neither — and holding a line open for a state that is
     usually not there would lengthen every card in the collection to suit the
     rare one. A viewer with no rights simply gets shorter cards, uniformly. */
  .mu-body { display: flex; flex-direction: column; gap: 5px; padding: 10px 12px 12px; }
  .mu-t { font-weight: 640; font-size: 0.84rem; line-height: 1.35; color: var(--text);
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden; min-height: 2.7em; }
  /* Channel and date on their own lines. They shared one flex row and wrapped,
     so a short channel sat beside its date and a long one pushed it onto a
     second line — which is why no two cards in the same row were the same
     height. One line each, ellipsis, reserved even when the probe has not
     answered yet and there is nothing to put in them. */
  .mu-ch, .mu-date { font-size: 0.71rem; color: var(--text-3); min-height: 1.2em;
    line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .mu-ch { color: var(--text-2); }
  /* ONE row of tags, and scroll for the rest. Most songs have one singer, a
     few have two or three, and holding a second row open for the one in twenty
     that needs it cost every other card twenty-four pixels of nothing. The
     scroll is still there — wrapped rows, one visible at a time — so nothing
     is hidden, it just is not paid for up front. */
  /* One line, clipped, and NOT scrollable.
     A 20px scroller is a scroller you fight: the wheel lands on it by accident
     while the page is what you meant to move, and reaching a fifth singer means
     dragging a bar the height of the text it hides. So the row shows what fits,
     hovering it shows the rest, and clicking it holds that open so the rest can
     be touched — see .mu-pop below. The fade on the right edge is the only thing
     that says there is more, which is exactly what a cut chip already implies
     and this makes deliberate. */
  .mu-tags { --mu-tagrow: 20px; display: flex; flex-wrap: nowrap; gap: 4px;
    margin: 1px -4px 0; padding: 0 4px; align-items: center;
    height: var(--mu-tagrow); overflow: hidden; border-radius: 7px;
    transition: background 0.13s ease, box-shadow 0.13s ease;
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 22px), transparent);
    mask-image: linear-gradient(90deg, #000 calc(100% - 22px), transparent); }
  /* The ROW lights up, not the chip under the pointer. The panel is about the
     whole row's contents, so the row is what has to look hoverable — and a
     highlight that followed the chips would flicker its way across the strip as
     the cursor crossed the 4px gaps between them, which is the same reason the
     panel itself is bound to the row. The padding and the negative margin that
     cancels it are permanent, so lighting up moves nothing. */
  .mu-tags.mu-lit { background: color-mix(in srgb, var(--accent) 9%, var(--surface-2));
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 24%, transparent); }
  /* Inside the add window the strip is the thing you are USING, and there is a
     whole panel of room, so it wraps and shows everything. */
  .mu-minetags { flex-wrap: wrap; height: auto; overflow: visible;
    -webkit-mask-image: none; mask-image: none; }
  .mu-minetags.mu-lit { background: none; box-shadow: none; }
  /* The shelf's strip answers a click, so it says so — but only when there is
     something behind the fade to reach. An empty bar whose only control is the
     `+ tag` button has that button's own cursor and needs none of its own. */
  .mu-tags.mu-has { cursor: pointer; }
  /* flex: none, so a chip is never squeezed narrower than its own text and the
     row cuts at the container edge instead of at every chip at once. */
  .mu-tag { height: 20px; display: inline-flex; align-items: center; flex: none;
    max-width: 148px; }
  .mu-tag { padding: 0 7px; border-radius: 20px; font-size: 0.68rem;
    border: 1px solid var(--border); color: var(--text-2); white-space: nowrap; }
  /* The NAME is what gets cut, never the × beside it. Without this a long
     enough tag pushes its own remove button out past the chip's edge, where it
     is unclickable and looks like a rendering fault. */
  .mu-tag .mu-tagn { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    min-width: 0; }
  /* The × on a chip and the `+ tag` at the end of the strip. Same two
     affordances the snippet strip has, sized for a chip that is 20px tall
     rather than for a row that is 118. */
  /* Every tag on the song, on hover, because the row itself can only ever show
     the first few. Fixed-position and appended to the body: the card it belongs
     to is inside a grid with clipped children, so anything positioned within it
     would be cut by the very overflow this exists to escape. */
  .mu-pop { position: fixed; z-index: 94; display: flex; flex-wrap: wrap; gap: 4px;
    max-width: 320px; padding: 7px 8px; border-radius: 10px;
    background: var(--panel); border: 1px solid var(--surface-3);
    box-shadow: 0 10px 26px rgba(0,0,0,0.5); pointer-events: none; }
  .mu-pop[hidden] { display: none; }
  .mu-pop .mu-tag { max-width: none; }
  /* Clicked rather than hovered, and then it is a control surface rather than a
     readout. The row is 20px of a strip that can hold six tags: the second tag's
     × is under the fade and the fifth is not inside the box at all, so it cannot
     be clicked and reads as a broken button rather than an absent one. Pinned,
     this panel is where those ×s are, which is the one state it takes the
     pointer in — hovered it must not, or the panel would steal the pointer from
     the row it is anchored to and flicker itself shut. */
  .mu-pop.pin { pointer-events: auto;
    border-color: color-mix(in srgb, var(--accent) 42%, var(--surface-3));
    box-shadow: 0 10px 26px rgba(0,0,0,0.5),
      0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent); }

  .mu-tagx { margin: 0 -3px 0 4px; padding: 0 2px; border: 0; background: none;
    color: var(--text-3); font: inherit; line-height: 1; cursor: pointer;
    border-radius: 4px; }
  .mu-tagx:hover { color: var(--tl-not); background: var(--surface-2); }
  /* Wore `.mu-tag` for its pill shape and then undid its border and its fill.
     States the shape itself now, and takes the family's accent hover rather
     than the half-step to `--text-2` it had alone — it was the only one of the
     six that did not go to accent. */
  .mu-tagadd { height: 20px; display: inline-flex; align-items: center;
    flex: none; padding: 0 7px; border-radius: 20px; font-size: 0.68rem;
    white-space: nowrap; }
  .mu-tag i { display: inline-block; width: 6px; height: 6px; border-radius: 50%;
    margin-right: 5px; vertical-align: 1px; }
  /* What the archive knows about this row, in one word. `preserved` is the
     only one that is about the FILE rather than about the verdict, and it is
     the whole reason the module exists — so it gets the accent. */
  .mu-chip { padding: 1px 6px; border-radius: 5px; font-family: var(--font-mono);
    font-size: 0.62rem; border: 1px solid var(--border); color: var(--text-3); }
  /* Top right of the cover, where the index puts its badges — the frosted
     square reads over artwork of any brightness, which is the whole reason it
     has one. Opposite corner from the duration, so neither ever covers the
     other. */
  .mu-kept { position: absolute; top: 6px; right: 6px; z-index: 1; }
  .mu-chip.wait { color: var(--text-3); }
  .mu-chip.bad { color: var(--bad, #e5484d);
    border-color: color-mix(in srgb, var(--bad, #e5484d) 45%, transparent); }
  .mu-acts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
  /* The rare way in, under the ordinary one and visibly second. A top border
     rather than a box: it is the same window and the same act, not a separate
     tool, and a bordered panel inside a bordered panel reads as a form within
     a form. */
  .mu-file { margin-top: 12px; padding-top: 10px;
    border-top: 1px solid var(--glass-line); }
  .mu-file button { margin-top: 2px; }
  /* Square, because they are glyphs now. A row of five verbs spelled out was
     wider than the card and read as a paragraph; the icons are the same five
     decisions and the tooltip carries the word. */
  /* THE WHOLE RULE SET MOVED TO `.btn-ent`, which this card was the template
     for — it is the base now and the music card has nothing left to say
     about an ordinary square. Only the one below, which is not one. */
  /* Text buttons survive in the player's header. */
  .mu-mhead .btn-ent { width: auto; height: auto; padding: 5px 12px;
    font-size: 0.76rem; color: var(--text-2); }
  .mu-mine { margin-left: 7px; color: var(--accent); }
  .mu-count { color: var(--text-3); font-size: 0.76rem; white-space: nowrap; }
  /* Narrower than the snippets one, which owns its whole toolbar. Here it
     shares the row with the three piles, and those have to stay readable. */
  /* No max-width: the search fills the bar and pushes the way-in button to
     the right end of it, which is where the Snippets toolbar puts its own.
     Capped, the button sat halfway across the panel beside a stub of a box. */
  .mu-search { flex: 1 1 220px; }
  .mu-search input { padding: 9px 12px 9px 34px; font-size: 0.82rem; }
  .mu-search svg { left: 11px; }
  .mu-empty { grid-column: 1 / -1; padding: calc(var(--vh) * 12) 0; text-align: center;
    color: var(--text-3); font-size: 0.82rem; }
  /* The player. A dialog rather than an inline swap, because the grid is the
     thing you came back to and an embed that replaced a card would lose your
     place in it. */
  .mu-modal { position: fixed; inset: 0; z-index: 90; display: grid;
    place-items: center; padding: 24px; background: rgba(0,0,0,0.72); }
  .mu-modal[hidden] { display: none; }
  .mu-frame { width: min(960px, 100%); }
  /* `video` alongside `iframe`, one rule: the archive's own copy of a song
     plays in the same box the embed does, and a second rule beside this one is
     how the two drift a border-radius apart. `aspect-ratio` rather than a
     height so a portrait upload letterboxes inside the frame instead of
     stretching it. */
  .mu-frame iframe, .mu-frame video { width: 100%; aspect-ratio: 16 / 9; border: 0;
    border-radius: var(--radius); background: #000; display: block; }
  .mu-mhead { display: flex; align-items: baseline; gap: 10px; margin-bottom: 9px;
    color: var(--text); }
  .mu-mhead b { font-size: 0.95rem; font-weight: 650; }
  .mu-mhead span { color: var(--text-3); font-size: 0.76rem; }
  .mu-mhead button { margin-left: auto; }

  /* ---- the changeset queue ------------------------------------------------
     One card per changeset, not per change row. A mint-and-attach is four
     rows that mean one thing a person did, and reviewing it four times is
     reviewing it wrong — the card is the unit a verdict applies to, so it is
     the unit on screen. */
  .cs-list { display: flex; flex-direction: column; gap: 8px; }
  .cs { display: grid; grid-template-columns: 1fr auto; gap: 6px 14px;
    align-items: start; padding: 12px 14px; border-radius: 12px;
    border: 1px solid var(--border); background: var(--surface); }
  .cs.busy { opacity: 0.5; pointer-events: none; }
  .cs-going { opacity: 0; transform: translateX(18px);
    transition: opacity 0.2s ease, transform 0.2s ease; }
  .cs-why { font-weight: 650; font-size: 0.95rem; color: var(--text); }
  .cs-who { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-3);
    white-space: nowrap; padding-top: 3px; }
  /* The changes themselves, in sentences. Monospace for the values only —
     the line is prose, and setting the whole thing in mono makes a queue of
     ordinary editorial decisions read like a diff. */
  .cs-what { grid-column: 1 / -1; margin: 2px 0 0; padding: 8px 10px;
    border-radius: 8px; background: var(--glass-sunk); font-size: 0.78rem;
    color: var(--text-2); display: flex; flex-direction: column; gap: 4px; }
  .cs-what b { color: var(--text); font-weight: 620; }
  .cs-what em { font-style: normal; font-family: var(--font-mono);
    font-size: 0.72rem; color: var(--text); }
  .cs-what .cs-gone { color: var(--text-3); }
  /* A verb chip, so the shape of a queue is readable before any of it is. */
  .cs-v { display: inline-block; min-width: 58px; margin-right: 7px; padding: 1px 6px;
    border-radius: 5px; font-family: var(--font-mono); font-size: 0.63rem;
    text-align: center; background: var(--glass-sunk); color: var(--text-3);
    border: 1px solid var(--border); }
  .cs-v.add { color: var(--tl-copyright); border-color: color-mix(in srgb, var(--tl-copyright) 45%, transparent); }
  .cs-v.cut { color: var(--bad, #e5484d); border-color: color-mix(in srgb, var(--bad, #e5484d) 45%, transparent); }
  /* Somebody edited the same field first. Loud, because approving over it is
     how a suggestion silently reverts an edit its author never saw. */
  .cs-clash { grid-column: 1 / -1; padding: 7px 10px; border-radius: 8px;
    font-size: 0.74rem; border: 1px solid color-mix(in srgb, var(--bad, #e5484d) 40%, transparent);
    background: color-mix(in srgb, var(--bad, #e5484d) 9%, transparent); color: var(--text-2); }
  .cs-acts { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px;
    align-items: center; margin-top: 2px; }
  .cs-acts .lbl { color: var(--text-3); font-size: 0.72rem; margin-right: 2px; }
  .cs-btn { padding: 5px 12px; border-radius: 8px; cursor: pointer; font: inherit;
    font-size: 0.76rem; border: 1px solid var(--border); background: var(--surface-2, transparent);
    color: var(--text-2); }
  .cs-btn:hover { color: var(--text); border-color: var(--text-3); }
  .cs-btn.yes { background: var(--accent); color: var(--on-accent); border-color: transparent;
    font-weight: 600; }
  .cs-btn.yes:hover { filter: brightness(1.08); }

  /* ---- the question queue -------------------------------------------------
     Deliberately near-identical to a changeset card and deliberately not the
     same class: the two piles are read the same way and answered differently,
     and sharing the class would mean the day one of them grows a control the
     other must not have, it grows it in both. Same padding, same radius, same
     rhythm — a reviewer moving between the tabs should not have to re-learn
     where to look. */
  .q-list { display: flex; flex-direction: column; gap: 8px; }
  .q { display: grid; grid-template-columns: 1fr auto; gap: 6px 14px;
    align-items: start; padding: 12px 14px; border-radius: 12px;
    border: 1px solid var(--border); background: var(--surface); }
  .q.busy { opacity: 0.5; pointer-events: none; }
  .q-going { opacity: 0; transform: translateX(18px);
    transition: opacity 0.2s ease, transform 0.2s ease; }
  /* Which entry, first and in the strongest weight on the card. The question
     itself is a sentence about a file; without the entry above it the reader
     has to hold four identical-sounding sentences apart by memory. */
  .q-head { font-weight: 650; font-size: 0.95rem; color: var(--text); }
  .q-head .q-idx { font-family: var(--font-mono); font-size: 0.8rem;
    color: var(--text-3); margin-right: 7px; }
  .q-when { font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-3);
    white-space: nowrap; padding-top: 3px; }
  .q-msg { grid-column: 1 / -1; margin: 2px 0 0; padding: 8px 10px;
    border-radius: 8px; background: var(--glass-sunk); font-size: 0.82rem;
    color: var(--text-2); }
  .q-acts { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px;
    align-items: center; margin-top: 2px; }
  .q-acts .lbl { color: var(--text-3); font-size: 0.72rem; margin-right: 2px; }
  /* Pushed to the far end, because it is not a fourth answer. Sitting in the
     row it reads as one, and the one thing this card must not do is offer a
     way out that looks like a way to settle it. */
  .q-again { margin-left: auto; }

  /* The entry's own questions and work, in the record panel. Deliberately
     louder than the measurements under them: a clean reading is a thing you
     glance at, an unanswered question is a thing somebody is waiting on. */
  /* The audit tool. One surface, so the report is the page rather than a
     block squeezed under a form — the columns can be columns again. */
  /* ONE HEIGHT ACROSS THE ROW. `align-items: flex-end` lined the boxes up by
     their bottoms, which is not the same thing: a <select>, a .btn-primary
     and a .cs-btn have three different intrinsic heights, so bottom-aligning
     them left three different tops. The label stacks its caption above its
     control, so the row is centred and the label's own bottom margin is what
     used to pass for alignment. */
  .au-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
            margin: 10px 0 14px; }
  .au-bar > .tp-fld { margin: 0; }
  /* The FIELDS are 38; the buttons are their own pair at 32 and sit on the
     same baseline. They were in this group when the primary button was the
     only thing beside the picker and matching its height was the only way to
     look deliberate. */
  .au-bar select, .au-bar input, .au-bar .cs-btn {
    height: 38px; box-sizing: border-box; }
  /* `inline-flex` because a fixed height does not centre a block's text:
     `.btn-primary` carries 12px of vertical padding, so `height: 38px` left
     `Audit` sitting low in its own button.
     THE MONO IS GONE. It was this panel's own override and nothing else's —
     every other primary button in the archive wears the page face, so the one
     on this bar read as a different kind of control rather than as the same
     one in a different room. */
  .au-bar .btn-primary, .au-bar .cs-btn { align-self: flex-end;
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0 16px; }
  /* ── THE PAIR AT THE END OF THE BAR ───────────────────────────────────
     Both 32 and both bottom-aligned, which is the whole of what was wrong:
     `.btn-ent` is 30x30 everywhere else in the archive because everywhere
     else it sits in a strip of its own kind, and next to a 38px primary
     button it read as undersized and floating. They are a pair here — the
     verb and the thing beside it — so they are sized as one. */
  .au-bar .btn-primary { height: 32px; gap: 8px; }
  .au-bar .btn-primary svg { width: 16px; height: 16px; flex: none; }
  .au-bar .btn-ent { width: 32px; height: 32px; align-self: flex-end; }
  .au-bar .btn-ent svg { width: 16px; height: 16px; }
  /* Sized for a number box, and then a <select> holding "#748 · maemi does
     recordings and schedule :D" moved in. `min-width: 0` is the load-bearing
     half: without it a flex item refuses to shrink below its content and the
     picker simply grew out from under the Audit button. */
  .au-idx { flex: 1 1 16rem; min-width: 0; max-width: min(32rem, 100%); }
  .au-idx select { width: 100%; min-width: 0; }
  .au-idx input { max-width: 9rem; }
  .au-state { font-size: 13px; color: var(--fg-dim, #8a8a8a); align-self: flex-end;
    padding-bottom: 10px; }
  .au-state.ok   { color: var(--ok, #30a46c); }
  .au-state.warn { color: var(--warn, #f5a524); }
  .au-state.bad  { color: var(--bad, #e5484d); }
  /* The number the vault gets, given the weight it has: it is the one figure
     an audit exists to propose, and it used to be absent entirely. */
  /* `.au-prop` has no markup any more — the duration moved into the metadata
     card's `video` half. Kept rather than cut: it is the archive's shape for
     "one proposed number with its provenance beside it", and the audit panel
     is the surface most likely to want a second one. */
  .au-prop { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
             margin: 14px 0 6px; }
  .au-prop > span { font-size: 12px; text-transform: uppercase;
                    letter-spacing: .06em; color: var(--fg-dim, #8a8a8a); }
  .au-prop > b { font-size: 20px; font-variant-numeric: tabular-nums; }
  .au-prop > i { font-style: normal; font-size: 12px; color: var(--fg-dim, #8a8a8a); }
  .au-prop > em.warn { font-style: normal; font-size: 12px;
                       color: var(--warn, #f5a524); }
  /* The source's number, when it does not match the file's. Marked, not
     corrected: which of the two is wrong is the thing nobody here knows. */
  .au-odd { color: var(--warn, #f5a524); }


  .au-take { margin-left: 6px; font-size: 11px; padding: 1px 6px; }

  /* The form's own row, under the records it commits. */
  .au-save { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
    margin: 4px 0 10px; }
  .au-save .au-dim { flex: 1 1 240px; min-width: 0; font-size: 0.68rem; }
  .au-ok { border-color: var(--accent); color: var(--accent); }
  .au-ok[disabled] { opacity: .45; cursor: not-allowed;
    border-color: var(--glass-line); color: var(--text-3); }
  /* A box on a record, sized to what it holds rather than to the column. */
  /* A value, not a field — until it is one. The same idiom as `.snip`: a
     transparent border that appears under the pointer, so the card reads as a
     report with things you CAN change rather than as a form with nine inputs
     on it. Opened by double-click; see the dblclick handler. */
  .au-in { background: none; border: 1px solid transparent;
    transition: border-color 0.15s ease, background-color 0.15s ease;
    color: var(--text); font-family: var(--font-mono); font-size: 0.7rem;
    padding: 3px 6px; border-radius: 7px; min-width: 0; }
  .au-in[readonly] { cursor: default; }
  .au-in[readonly]:hover { border-color: var(--glass-line); }
  /* Open: now it looks like the box it has become. */
  .au-in:not([readonly]) { background: var(--glass-sunk);
    border-color: var(--accent); cursor: text; }
  .au-in:focus { outline: none; }
  /* The length, beside the start it belongs to. */
  .au-par { color: var(--text-3); font-family: var(--font-mono); font-size: 0.68rem; }
  /* A glyph standing in for a column heading. */
  .au-dtico { display: grid; place-items: center; color: var(--text-3); }
  .au-dtico svg { display: block; width: 15px; height: 15px; }
  .au-vid { flex: 1 1 110px; }
  .au-when { width: 152px; flex: none; }
  .au-rech { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
  .au-plat { flex: 0 0 34px; min-height: 26px; margin: 0; }
  /* One file: what it is, what it weighs, what it is called, what you can do
     to it. The name takes the slack, because it is the only part whose length
     is not known in advance. */
  .au-file { display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
    margin: 5px 0; font-size: 0.68rem; }
  /* A glyph where the words were. `.au-fb` is next to it and the name after
     that, so what the row is about is read from the picture and the size. */
  .au-fl { flex: none; display: grid; place-items: center; color: var(--text-3); }
  .au-fl svg { display: block; width: 15px; height: 15px; }
  /* SMALLER INSIDE A CARD. `.cs-btn` is sized for a panel footer — 5px of
     padding at 0.76rem stands taller than the 0.68rem row it sits in and
     bulges out of it. Scoped to the card rather than changed at source,
     because every other panel's buttons are the right size already. */
  .au-rec .cs-btn { padding: 2px 8px; font-size: 0.68rem; line-height: 1.35; }
  .au-rec .au-ref { padding: 2px 5px; line-height: 0; }
  /* The picker, beside the name it changes. Narrow, because it is an
     ellipsis — the same affordance the record editor's path rows use. */
  .au-rec .au-pick { flex: none; padding: 2px 7px; }
  /* The browse panel opens UNDER the row it belongs to. `.au-rec` is a plain
     block, so it simply flows there — the `grid-column` that was here did
     nothing, because nothing around it is a grid. */
  .au-file + .rec-browse { margin: 2px 0 8px; }
  /* `pre`, or the padding in mbCol() collapses to one space and the column
     it exists to make goes with it. */
  .au-fb { font-family: var(--font-mono); color: var(--text-2); white-space: pre; }
  .au-fn { flex: 1 1 140px; min-width: 0; font-family: var(--font-mono);
    color: var(--text-3); white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; }
  /* Marked, not gone. It is still on disk and still listed, and Cancel puts
     it back — so it is struck through rather than removed. */
  .au-doomed { opacity: .6; }
  .au-doomed .au-fn, .au-doomed .au-fb { text-decoration: line-through; }
  .au-doomed .au-del { border-color: var(--accent); color: var(--accent); }
  .au-ref { padding: 3px 6px; line-height: 0; }
  .au-ref svg { display: block; width: 15px; height: 15px; }
  /* Required rather than merely available: the id under it has changed and
     Save is shut until this has answered. */
  /* THE STALE MARK, which used to be a border colour — and `.btn-op` has no
     border to colour. Said in the two things it does have instead: the accent
     on the glyph, and a faint wash behind it so the button reads as asking
     for something rather than merely sitting there. */
  .au-need { color: var(--accent);
    background: color-mix(in srgb, var(--accent) 12%, transparent); }
  .au-need:hover { color: var(--accent);
    background: color-mix(in srgb, var(--accent) 20%, transparent); }
  /* AN OPERATOR THAT IS AN ANCHOR. It opens the recording on the platform,
     which is a `<a href>` and has to stay one — middle-click, copy link
     address and "open in new tab" are the whole reason somebody reaches for
     it, and none of them survive being a button with a click handler.
     So it wears `.btn-op` in the markup and only says here what an anchor
     brings that a button does not: an underline to turn off, and a
     `place-items` because the glyph is its only content. The accent on hover
     is kept over the base's plain `--text` because this one leaves the page,
     and that is worth saying in the colour. */
  .au-out { flex: none; display: grid; place-items: center;
    text-decoration: none; }
  .au-out svg { display: block; width: 14px; height: 14px; }
  .au-out:hover { color: var(--accent); background: none; }
  /* One line, cut. A stream title is routinely 120 characters here. */
  .au-title { margin: 6px 0 2px; font-size: 0.72rem; color: var(--text-2);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* ---- the verdict beside what the entry IS ----------------------------
     The left column is the audit — the mark, what was asked, what is
     running. The right is the entry's own metadata, which is a card and not
     a list because it is the same kind of object as the records below it.
     Stretched rather than `start`, so the card's border runs the height of
     whichever side is taller instead of stopping halfway down a column. */
  .au-top { display: grid; gap: 12px; align-items: stretch; margin: 0 0 10px;
            grid-template-columns: 1fr 1fr; }
  .au-side { min-width: 0; }
  .au-meta { min-width: 0; }
  /* `chat` and `video`, which are the two halves of what an entry has. They
     are rows of the card's own `<dl>` spanning both columns, not headings
     above two separate lists — two grids size their label columns
     independently, which is how `duration` and `messages` ended up starting
     at different x. The `.au-kv dt` rules are out-specified here on purpose:
     this is a section mark that happens to be a `dt`, not a label. */
  .au-meta .au-kv > .au-sub, .au-meta .au-kv > .au-wide { grid-column: 1 / -1; }
  .au-meta .au-kv > .au-sub { font-family: var(--font-mono); font-size: 0.58rem;
    letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-3);
    margin: 10px 0 1px; }
  .au-meta .au-kv > .au-sub:first-child { margin-top: 2px; }
  /* Rebuild deletes the file on this row, so it sits on it. */
  .au-fileln { display: flex; align-items: center; gap: 8px; min-width: 0; }
  .au-fileln > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .au-fileln .au-del { flex: none; }

  .au-recs { display: grid; gap: 12px; margin: 10px 0 4px;
             grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
  /* THE WAY IN, drawn as the thing it makes. A record-sized box in the row of
     records, dashed because it is an outline where a record is not yet —
     which is what every other `btn-add` in this archive means by a dash. */
  .au-plus { border: 1px dashed var(--dash-line); border-radius: 6px;
    background: none; color: var(--text-3); cursor: pointer;
    display: grid; place-items: center; min-height: 96px;
    transition: border-color 0.16s ease, color 0.16s ease; }
  .au-plus:hover { border-color: var(--accent); color: var(--accent); }
  .au-plus svg { width: 30px; height: 30px; }
  .au-rec { border: 1px solid var(--line, #2a2a2a); border-radius: 6px;
            padding: 10px 12px; }
  .au-rech { font-size: 13px; margin-bottom: 6px; }
  /* The sync's answer, on the card it would correct. Amber rather than red on
     a refusal: nothing went wrong, the two recordings could not be matched —
     which is itself the finding when the cause is a cut. */
  .au-syncrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    margin: 0 0 8px; padding: 7px 9px; border-radius: 8px; font-size: 0.74rem;
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent) 26%, transparent); }
  .au-syncrow .cs-btn { margin-left: auto; }
  .au-syncrow .cs-btn + .cs-btn { margin-left: 0; }
  .au-syncrow.is-bad { background: color-mix(in srgb, var(--tl-meta) 9%, transparent);
    border-color: color-mix(in srgb, var(--tl-meta) 26%, transparent); }
  .au-syncrow.is-run { background: var(--glass-2); border-color: var(--glass-line);
    color: var(--text-3); }
  .au-syncrow .up-bar { flex: 0 0 60px; margin: 0; }
  /* ── THE CLOCK CHIP ─────────────────────────────────────────────────────
     `btn-op au-ref`, which is the box Fetch and the open-link beside it wear,
     so the three sit on one line at one size. It was `rec-f rec-pico` — the
     record editor's 34px field chip — which is a FIELD and not a button, and
     in a row of 26px buttons it stood proud of both its neighbours.
     THE SELECT IS STILL THE CONTROL, so the two things `rec-pico` was
     providing have to come along: the overlay, and a focus ring on the
     WRAPPER — the element actually focused is at zero opacity, and an outline
     on that is an outline nobody can see. */
  .au-syncwrap { position: relative; display: grid; place-items: center;
    flex: none; padding: 0; }
  .au-syncwrap select { position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; cursor: pointer; appearance: none; -webkit-appearance: none;
    border: 0; padding: 0; margin: 0; }
  .au-syncwrap .au-syncico { display: grid; place-items: center;
    pointer-events: none; }
  .au-syncwrap .au-syncico svg { display: block; }
  .au-syncwrap:focus-within { outline: 2px solid var(--air); outline-offset: 1px; }
  /* THE MENU ITSELF, as far as a native one can be dressed. Firefox takes the
     background and colour off the select and its options and uses them for
     the popup; a chrome that cannot be styled falls back to its own, which is
     why this is a courtesy and not a layout. Without it the menu opens white
     on a panel that is not. */
  .au-syncwrap select, .au-syncwrap option {
    background: var(--panel); color: var(--text);
    font-family: var(--font-ui); font-size: 0.8rem; }
  .au-syncwrap option:disabled { color: var(--text-3); }
  .au-rech a { color: inherit; }
  /* Two columns so the labels line up down the card and the numbers can be
     compared across cards, which is the entire point of putting the two
     recordings side by side. */
  .au-kv { display: grid; grid-template-columns: auto 1fr; gap: 3px 10px;
           margin: 0; font-size: 13px; }
  .au-kv dt { color: var(--fg-dim, #8a8a8a); }
  .au-kv dd { margin: 0; font-variant-numeric: tabular-nums; }
  @media (max-width: 560px) { .au-recs { grid-template-columns: 1fr; } }
  @media (max-width: 760px) { .au-top { grid-template-columns: 1fr; } }

  .au-ask { border-left: 2px solid var(--warn, #f5a524); padding: 6px 0 6px 8px;
            margin: 6px 0; }
  .au-askmsg { font-size: 13px; line-height: 1.45; }
  .au-askacts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
  .au-job { display: grid; grid-template-columns: auto 1fr auto; gap: 6px;
            align-items: baseline; padding: 5px 0 5px 8px; margin: 4px 0;
            border-left: 2px solid var(--line, #2a2a2a); font-size: 13px; }
  .au-job.note { border-left-color: var(--fg-dim, #8a8a8a); }
  .au-job.warn { border-left-color: var(--warn, #f5a524); }
  .au-job.bad  { border-left-color: var(--bad, #e5484d); }
  .au-job.ok   { border-left-color: var(--ok, #30a46c); }
  .au-jn { font-weight: 600; }
  /* The sentence, not a tooltip. A person who has just pressed a button and
     is asking "did that do anything" should not have to discover that the row
     has a hover state. The title attribute stays for the same text, because
     the row can be narrow. */
  .au-why { grid-column: 1 / -1; font-size: 12px; line-height: 1.45;
            color: var(--fg-dim, #8a8a8a); }
  .au-ask.busy, .au-job.busy { opacity: .55; pointer-events: none; }
  /* WHICH ENTRY, on the list that spans them. A button and not a label: the
     next thing anybody wants after reading the question is the entry it is
     about, and making them retype the number into the field above would be
     the second surface this list exists to remove. */
  .au-askwho { display: block; margin-bottom: 3px; padding: 0; border: 0;
    background: none; font: inherit; font-size: 0.7rem; letter-spacing: .02em;
    color: var(--text-3); cursor: pointer; text-align: left; max-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .au-askwho:hover { color: var(--accent); }
  /* The one answer this archive cannot take back for you. */
  .q-cut { color: var(--bad, #e5484d);
    border-color: color-mix(in srgb, var(--bad, #e5484d) 45%, transparent); }
  .q-cut:hover { color: var(--bad, #e5484d);
    border-color: var(--bad, #e5484d); }
  /* ── the read-back ────────────────────────────────────────────────────
     One row per field: name, value, why. A GRID rather than three floated
     spans, so the values line up down the column and a long reason wraps
     under itself instead of pushing the value out of line. */
  .au-blk { border: 1px solid var(--line, #262626); border-radius: 6px;
    margin: 6px 0; overflow: hidden; }
  .au-blkh { display: flex; align-items: baseline; gap: 8px;
    padding: 5px 8px; background: var(--bg-soft, #141414);
    border-bottom: 1px solid var(--line, #262626); font-size: 11px;
    letter-spacing: .06em; }
  /* WAS ALSO `.au-ref`, which is a BUTTON four hundred lines up. One class
     on two unrelated elements: the byline was picking up the fetch button's
     `padding: 3px 6px; line-height: 0` and the operator family's mono and
     upper-casing, and this line was the half of that somebody had noticed and
     undone (`letter-spacing: 0`, a smaller size). Renamed rather than undone
     further — a byline is not a control and should not have to cancel one. */
  .au-by { color: var(--fg-dim, #8a8a8a); font-size: 10.5px; }
  .au-blkr { margin-left: auto; }
  .au-ask1 { font-size: 10.5px; padding: 1px 7px; }
  /* A proposed value is an INPUT, because revising it is typing in it. Styled
     to sit flush in the row rather than as a form field: it is the value,
     shown where the value goes, and it happens to be editable. */
  .au-edit { font: inherit; font-size: 12px; width: 100%; min-width: 0;
    padding: 1px 6px; border-radius: 4px; color: var(--text);
    background: var(--bg-soft, #141414);
    border: 1px solid color-mix(in srgb, var(--line, #262626) 80%, transparent); }
  .au-edit:focus { border-color: var(--accent, #e93d82); outline: none; }
  .au-csacts { display: flex; align-items: center; gap: 8px;
    padding: 6px 8px; border-top: 1px solid var(--line, #262626); }
  .au-blk.busy { opacity: .55; pointer-events: none; }
  /* The throbber. Monospace and a fixed width so the line does not jiggle as
     the frames change width, which braille cells do not — but the font might
     substitute, and a row that twitches once a frame is worse than no
     throbber at all. */
  .spin { font-family: var(--mono, ui-monospace, monospace); font-style: normal;
    display: inline-block; width: 1em; text-align: center; opacity: .85; }
  .au-f { display: grid; grid-template-columns: 150px minmax(0, 1fr) minmax(0, 1.1fr);
    gap: 8px; padding: 3px 8px; align-items: baseline; font-size: 12px; }
  .au-f + .au-f { border-top: 1px solid color-mix(in srgb, var(--line, #262626) 55%, transparent); }
  .au-fk { color: var(--fg-dim, #8a8a8a); font-family: var(--mono, ui-monospace, monospace);
    font-size: 11px; overflow-wrap: anywhere; }
  .au-fw { color: var(--fg-dim, #8a8a8a); font-size: 11px; line-height: 1.35; }
  /* Under the facts, not among them: these SPEND something, and a row of
     buttons inline with the numbers reads as more numbers. */
  .au-act { flex: none; }
  /* The throbber now follows the file rows rather than a button row. */
  .au-file + .up-bar { margin-top: 6px; }
  .au-act[disabled] { opacity: .45; cursor: not-allowed; }
  /* The sweep belongs to the card whose errand is running, so two cards
     working at once are told apart. */
  .au-src { display: block; opacity: .75;
    font-family: var(--mono, ui-monospace, monospace); font-size: 10.5px; }
  /* Dimmer than the source it hangs under, and dimmer again when nothing
     corroborated: this is the qualifier on a number, not the number. */
  .au-q { display: block; font-size: 10.5px; opacity: .7; }
  .au-q.thin { color: var(--warn, #f5a524); opacity: .85; }
  /* A reading of something that is not there any more. Dimmer than a warning
     — nothing is wrong, the file was deleted on purpose — and distinct from
     a live one, because a number nobody can re-check should not sit at the
     same weight as one that was taken this minute. */
  .au-q.gone { color: var(--fg-dim, #8a8a8a); font-style: italic; }
  /* AGREES is a pill and nothing else. No tick: twenty agreeing rows is the
     ordinary case, and a mark on each is twenty marks to read past before
     reaching the one row that does not agree. */
  .au-pill { display: inline-block; padding: 1px 7px; border-radius: 10px;
    border: 1px solid transparent; }
  .au-pill.ok { color: var(--ok, #30a46c);
    border-color: color-mix(in srgb, var(--ok, #30a46c) 35%, transparent);
    background: color-mix(in srgb, var(--ok, #30a46c) 9%, transparent); }
  .au-pill.none { color: inherit; }
  /* DIFFERS: what is on file, struck, and what was measured beside it. Both,
     because "it should be X" without "it is currently Y" is not something
     anybody can check. */
  .au-was { text-decoration: line-through; opacity: .6; margin-right: 8px; }
  .au-now { color: var(--bad, #e5484d); }
  /* The em dash goes where a PROPOSAL would be, never where the value is.
     A dash in the value column is what made an entry whose numbers were
     perfectly safe in the database read as though it had been emptied. */
  .au-non { color: var(--fg-dim, #8a8a8a); font-style: normal; opacity: .7; }
  .au-drv { padding: 4px 8px; font-size: 10.5px; letter-spacing: .05em;
    color: var(--fg-dim, #8a8a8a);
    border-top: 1px solid var(--line, #262626);
    background: color-mix(in srgb, var(--line, #262626) 30%, transparent); }
  /* Derived rows never carry a verdict colour. They are consequences of the
     measurements above them, and colouring one would invite acting on it
     independently of its inputs. */
  .au-f.drv .au-pill, .au-f.drv .au-now { color: var(--fg-dim, #8a8a8a);
    border-color: transparent; background: none; }

  /* The one answer that spends something. Marked for the same reason `q-cut`
     is, in the other direction: the rest of the row files a fact, and this
     puts a download on the recorder. */
  .q-go { color: var(--ok, #30a46c);
    border-color: color-mix(in srgb, var(--ok, #30a46c) 45%, transparent); }
  .q-go:hover { color: var(--ok, #30a46c);
    border-color: var(--ok, #30a46c); }
  /* `identified` is the one answer carrying a value, so it is the one answer
     that is not a button on its own. The box and its button sit together so it
     reads as one control rather than a stray field beside two verbs. */
  .q-id { display: inline-flex; gap: 4px; align-items: center; }
  .q-id input { width: 160px; padding: 4px 8px; border-radius: 7px;
    border: 1px solid var(--border); background: var(--glass-sunk);
    color: var(--text); font-family: var(--font-mono); font-size: 0.72rem; }
  .q-id input:focus { outline: none; border-color: var(--text-3); }
  /* Whoever may read the queue but not answer it. Says so once per card
     instead of drawing buttons that 403. */
  .q-cant { grid-column: 1 / -1; font-size: 0.74rem; color: var(--text-3); }

  .rv-views { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px;
    background: var(--surface-2, rgba(255,255,255,0.05)); }
  .rv-view { padding: 5px 11px; border: 0; border-radius: 7px; cursor: pointer;
    background: none; color: var(--text-3); font: inherit; font-size: 0.78rem;
    transition: background 0.14s ease, color 0.14s ease; }
  .rv-view:hover { color: var(--text-2); }
  .rv-view.is-on { background: var(--surface); color: var(--text); font-weight: 600; }
  /* How much is waiting, on the tab itself — the queue is the one pile nobody
     goes looking for, because nothing anywhere else says it has anything in
     it. Hidden at zero rather than showing a 0. */
  .rv-sgn { margin-left: 6px; padding: 1px 5px; border-radius: 20px;
    background: var(--accent); color: var(--on-accent);
    font-family: var(--font-mono); font-size: 0.6rem; }
  /* The pile inside a pile. Quieter than .rv-views on purpose — it is a filter
     within the view you already chose, not a fifth view — and on its own line
     so it cannot be mistaken for another tab in the row above. */
  .rv-subs { display: inline-flex; gap: 2px; margin: 10px 0 0; padding: 2px;
    border-radius: 8px; background: var(--surface-2, rgba(255,255,255,0.05)); }
  /* The key line used to sit below here on a -12px margin and this rule
     cancelled the tuck whenever the switcher stood between them. The keys are
     inside the bar now, on the first row and pushed right, so there is
     nothing left to tuck under and nothing between them to collide with. */
  .rv-sub { padding: 3px 10px; border: 0; border-radius: 6px; cursor: pointer;
    background: none; color: var(--text-3); font: inherit; font-size: 0.74rem;
    transition: background 0.14s ease, color 0.14s ease; }
  .rv-sub:hover { color: var(--text-2); }
  .rv-sub.is-on { background: var(--surface); color: var(--text); font-weight: 600; }
  .rv-sub .rv-sgn { background: var(--surface-3); color: var(--text-2); }
  /* The same quiet row as `.rv-subs`, because it is the same kind of thing —
     a filter within the pile you already chose. Toggles and not tabs, so the
     on state is `aria-pressed` and more than one of them can hold it. */
  .rv-marks { display: inline-flex; flex-wrap: wrap; gap: 2px; margin: 10px 0 0;
    padding: 2px; border-radius: 8px;
    background: var(--surface-2, rgba(255,255,255,0.05)); }
  .rv-marks[hidden] { display: none; }
  .rv-marks .rv-sub[aria-pressed="true"] { background: var(--surface);
    color: var(--text); font-weight: 600; }
  /* A DOT IN THE BAND'S OWN COLOUR, so the chip and the badge on the row it
     filters for are the same thing twice rather than two things to learn. */
  .rv-marks .rv-sub i { width: 6px; height: 6px; border-radius: 50%;
    display: inline-block; margin-right: 6px; vertical-align: 1px; flex: none; }
  .rv-marks i.is-same { background: var(--tl-not); }
  .rv-marks i.is-resized { background: var(--tl-hold); }
  .rv-marks i.is-near { background: var(--tl-meta); }
  .rv-marks i.is-fix { background: var(--tl-not); border-radius: 1px; }
  .rv-marks i.is-ok { background: var(--text-3); opacity: 0.5; }
  /* The row under the cursor in the tombstone list. Same affordance as
     `.rv-at` on a clip, at the size of a one-line row: the accent edge is what
     says "D acts on this one", and D cannot be undone, so it has to be
     unmistakable which row it is about to act on. */
  #rv-dead .ops-row.rv-at { background: var(--accent-soft);
    box-shadow: inset 3px 0 0 var(--accent); }
  #rv-dead .ops-row.is-going { opacity: 0.4; }

  /* ---- lazy load ---------------------------------------------------------
     The sentinel is always in the DOM when there is a next page, and the
     observer's rootMargin means it is reached — and the fetch started — before
     it is on screen. So the spinner is usually never seen, which is the point;
     it exists for a slow request, not as the normal state. */
  .snip-sentinel { display: flex; align-items: center; justify-content: center; gap: 9px;
    padding: 20px; color: var(--text-3); font-size: 0.78rem; }
  .snip-spin { width: 13px; height: 13px; border-radius: 50%; display: inline-block;
    border: 2px solid var(--border); border-top-color: var(--text-3);
    animation: snip-spin 0.7s linear infinite; }
  @keyframes snip-spin { to { transform: rotate(360deg); } }
  @media (prefers-reduced-motion: reduce) { .snip-spin { animation: none; } }

  /* ---- taglet autocomplete ----------------------------------------------- */
  .snip-search-wrap { position: relative; }
  .snip-ac { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 60;
    max-height: 302px; overflow-y: auto; padding: 5px;
    background: var(--surface-2, var(--surface)); border: 1px solid var(--border);
    border-radius: 10px; box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5); }
  .snip-ac-row { display: flex; align-items: center; gap: 9px; width: 100%;
    padding: 7px 9px; border: 0; border-radius: 7px; background: none; cursor: pointer;
    color: var(--text); font: inherit; font-size: 0.82rem; text-align: left; }
  /* The kind is carried by a wash of its own colour across the whole row plus a
     bar down its leading edge, not a dot or a trailing label: at a glance down
     the list the reader is sorting by category, and colour is the only channel
     that survives being skimmed. The first version used 14% and washed out
     against the panel — a tint you have to look for is not doing the job. */
  .snip-ac-row[data-kind] { background: color-mix(in srgb, var(--tl) 26%, transparent);
    box-shadow: inset 3px 0 0 var(--tl); }
  .snip-ac-row:hover, .snip-ac-row.at {
    background: color-mix(in srgb, var(--tl, var(--accent)) 46%, transparent); }
  .snip-ac-row[data-kind="character"] { --tl: var(--tl-character); }
  .snip-ac-row[data-kind="copyright"] { --tl: var(--seg-copyright); }
  .snip-ac-row[data-kind="meta"]      { --tl: var(--tl-meta); }
  .snip-ac-row[data-kind="artist"]    { --tl: var(--tl-artist); }
  .snip-ac-row[data-kind="general"]   { --tl: var(--tl-general); }
  .snip-ac-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .snip-ac-name b { color: var(--tl); font-weight: 700; }
  .snip-ac-kind { font-size: 0.66rem; color: var(--text-3); text-transform: lowercase; }
  .snip-ac-n { font-family: var(--font-mono); font-size: 0.66rem; color: var(--text-3); }
  .snip-ac-free { color: var(--text-3); }
  .snip-ac-free b { color: var(--text); font-weight: 600; }
  /* The operator you typed is applied to the whole list, not to one row: what
     Tab is about to do has to be visible BEFORE the key is pressed, or the
     prefix is a mode with no indicator. */
  .snip-ac[data-mode="not"] .snip-ac-row { box-shadow: inset 3px 0 0 var(--tl-not); }
  .snip-ac[data-mode="not"] .snip-ac-name { text-decoration: line-through;
    text-decoration-color: var(--tl-not); }
  .snip-ac-hint { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 4px;
    padding: 7px 9px 3px; border-top: 1px solid var(--border);
    font-size: 0.68rem; color: var(--text-3); }
  .snip-ac-hint b { font-family: var(--font-mono); font-weight: 600; color: var(--text-2); }
  .snip-ac-none { padding: 9px; font-size: 0.78rem; color: var(--text-3); }

  /* ---- add-clip button ----------------------------------------------------
     Hollow: the accent as an outline and as the text, with only a wash of it
     inside. Adding a clip is not what anyone opened this panel to do — a solid
     accent block beside the search field reads as an instruction, and the
     button still has to be found without being the loudest thing on screen.
     11/15 padding, not 12/16, because the border it gained is a pixel too. */
  .snip-up-btn { display: inline-flex; align-items: center; gap: 6px;
    background: color-mix(in srgb, var(--accent) 13%, transparent);
    color: var(--accent);
    border: 1px solid color-mix(in srgb, var(--accent) 52%, transparent);
    padding: 11px 15px;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; }
  .snip-up-btn:hover { background: color-mix(in srgb, var(--accent) 24%, transparent);
    border-color: var(--accent); color: var(--accent); }

  /* ---- the upload tab -----------------------------------------------------
     The same button, shaped like the handle of the thing it opens. Flat and
     borderless on the right and pulled out through `.wrap`'s gutter so it
     meets the viewport edge the drawer arrives from; `body.up-open .toolbar`
     already adds the drawer's width as padding, so an open drawer carries the
     tab inwards with it and the handle stays on the panel.
     The negative margin exactly consumes the gutter it sits in, so this adds
     no scrollable width — and on the phone breakpoint, where that padding is
     dropped and the row wraps, it is turned off below with it. */
  .snip-up-btn.up-tab { border-radius: var(--radius-sm) 0 0 var(--radius-sm);
    border-right-color: transparent; padding-right: calc(15px + var(--gutter) / 2);
    margin-right: calc(var(--gutter) * -1); }

  /* ---- the tile-size switch ----------------------------------------------
     Glass, because it belongs to neither of its neighbours: the search is a
     field and the tab is an action, and this is a statement about what you
     are looking at. One of the two glyphs shows at a time — the corners for
     theater, the four panes for default — and the word beside it is the state
     it is IN. */
  /* 6px AND NOT 10, so this stands the same height as the shelf track it is
     now in front of — measured at 32.7px, which is `.seg`'s 2px padding and
     its buttons' 4px either side of a 0.78rem line. A row of three controls
     at three heights was the whole of what moving it fixed. */
  .gal-fit { display: inline-flex; align-items: center; gap: 7px;
    padding: 6px 14px; border-radius: 999px; cursor: pointer;
    font: inherit; font-size: 0.78rem; font-weight: 600; color: var(--text-2);
    transition: color 0.15s ease; }
  .gal-fit:hover { color: var(--text); }
  /* A PRESS THAT ANSWERS. The thing this button changes is off at the far
     side of the page — the tiles resize and a rail appears beside them — so
     the control itself had nothing to say about having been pressed. Down
     first and then a little past one: a squash and its overshoot is what a
     press feels like, and it is over in a third of a second.
     Turned off where the system asks for less motion, which is the only
     place in this sheet that animates on an idle screen. */
  @keyframes gf-bounce {
    0%   { transform: scale(1); }
    34%  { transform: scale(0.93); }
    66%  { transform: scale(1.05); }
    100% { transform: scale(1); }
  }
  .gal-fit:active { transform: scale(0.96); }
  .gal-fit.is-bounce { animation: gf-bounce 0.34s cubic-bezier(0.22, 1, 0.36, 1); }
  @media (prefers-reduced-motion: reduce) {
    .gal-fit.is-bounce { animation: none; }
    .gal-fit:active { transform: none; }
  }
  .gal-fit svg { flex: 0 0 auto; }
  .gal-fit .gf-df, .gal-fit[aria-pressed="true"] .gf-th { display: none; }
  .gal-fit[aria-pressed="true"] .gf-df { display: block; }
  .gal-fit[aria-pressed="true"] { color: var(--accent); }

  /* ---- the upload window --------------------------------------------------
     Anchored under the button that opened it, floating over the clips, and it
     takes nothing away: no scrim, no focus trap, and an outside click does NOT
     dismiss it. That last one is the difference between a window and a menu —
     a menu closes when you look elsewhere, and looking elsewhere is exactly
     what you do while a file uploads and you type its title. */
  /* `.pop2` is on this selector and not on a copy of it further down, which is
     the whole of "unify the minting surfaces". The first attempt WAS a copy —
     assembled out of the same tokens by hand — and it came out looking like
     the dark panel it replaced, because a hand copy gets to be subtly wrong:
     it used --bg-blur (0.78 alpha) instead of --glass (0.62), and it left off
     `inset 0 1px 0 0 var(--glass-hi)`, which is the lit top edge and is most
     of what actually reads as glass. Two selectors on one recipe cannot drift
     like that. Anything below that re-declares background, border or
     box-shadow on .pop2 would win on later-wins, so it declares none of them. */
  .glass, .pop2 {
    background: var(--glass);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    border: 1px solid var(--glass-line);
    box-shadow: var(--glass-shadow), inset 0 1px 0 0 var(--glass-hi);
  }
  /* Without this, Firefox with the pref off is a transparent rectangle with
     text floating on the posters — unreadable, not merely plainer. */
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .glass, .pop2 { background: color-mix(in srgb, var(--panel) 96%, #000); }
  }

  /* How many of yours are still in flight or still waiting, on the button that
     opens the window — so a closed window is not the same as no news. */
  .up-n { font-family: var(--font-mono); font-size: 0.62rem; padding: 1px 6px;
    border-radius: 20px; background: var(--accent); color: var(--on-accent);
    margin-left: 2px; }
  .snip-up-btn[aria-expanded="true"] { background: var(--accent); color: var(--on-accent);
    border-color: var(--accent); }
  .snip-up-btn[aria-expanded="true"] .up-n { background: var(--on-accent); color: var(--accent); }

  .up-win { position: fixed; z-index: 92; width: 424px; border-radius: var(--radius);
    display: flex; flex-direction: column; max-height: min(calc(var(--vh) * 78), 780px);
    opacity: 0; transform: translateY(-8px) scale(0.985); pointer-events: none;
    transform-origin: top right;
    transition: opacity 0.19s ease, transform 0.19s cubic-bezier(0.22, 1, 0.36, 1); }
  .up-win.open { opacity: 1; transform: none; pointer-events: auto; }
  /* Carried, not sitting. The blur and the big shadow are what a glass panel
     is FOR when it is at rest over the page; while it is moving they are two
     full-area repaints a frame and nobody is looking at the frosting anyway.
     The background is the same one the no-backdrop-filter fallback uses, so
     the swap is very hard to catch. */
  .up-win.is-drag { -webkit-backdrop-filter: none; backdrop-filter: none;
    background: color-mix(in srgb, var(--panel) 96%, #000);
    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.42);
    will-change: left, top; }
  @media (prefers-reduced-motion: reduce) { .up-win { transition: none; } }
  /* A single soft highlight raking the top, which is what stops a blurred
     rectangle from reading as frosted plastic. */
  .up-win::before { content: ""; position: absolute; inset: 0; border-radius: inherit;
    pointer-events: none;
    background: linear-gradient(180deg, rgba(255,255,255,0.055) 0%, rgba(255,255,255,0) 42%); }

  .up-head { display: flex; align-items: flex-start; gap: 10px; padding: 13px 14px 11px;
    border-bottom: 1px solid var(--glass-line); cursor: grab; user-select: none; }
  .up-head:active { cursor: grabbing; }
  .up-title { font-family: var(--font-display); font-weight: 600; font-size: 0.93rem; }
  .up-sub { font-size: 0.71rem; color: var(--text-3); margin-top: 1px; }
  .up-x { margin-left: auto; background: none; border: 0; color: var(--text-3);
    width: 25px; height: 25px; border-radius: 7px; display: grid; place-items: center;
    flex: none; cursor: pointer; }
  .up-x:hover { background: var(--glass-2); color: var(--text); }

  .up-body { flex: 1; min-height: 0; overflow-y: auto; padding: 13px 14px 15px; }
  .up-body::-webkit-scrollbar { width: 6px; }
  .up-body::-webkit-scrollbar-thumb { background: var(--glass-3); border-radius: 3px; }

  /* One target for a file and a link both. A File | URL switcher makes you
     pick a mode before you have done anything. */
  .up-drop { border: 1px dashed var(--glass-line); border-radius: 12px; padding: 16px 14px;
    text-align: center; background: rgba(255,255,255,0.022);
    transition: border-color 0.16s ease, background 0.16s ease; }
  .up-drop.hot { border-color: var(--accent); background: var(--accent-soft); }
  /* A LINK-ONLY WINDOW HAS NO DROP ZONE, so it should not be drawn as one. The
     dashed border and the tinted panel are the affordance — they say "things
     can be dropped here" — and left around a lone URL field they invite the
     one gesture this reader is not allowed to make. The field keeps the room
     it had; only the invitation goes. */
  .up-win[data-files="0"] .up-drop { border: 0; background: none;
    padding: 0; text-align: left; }
  .up-drop-i { color: var(--text-3); margin-bottom: 6px; }
  .up-drop.hot .up-drop-i { color: var(--accent); }
  .up-drop-t { font-size: 0.84rem; color: var(--text-2); }
  .up-drop-t b { color: var(--text); font-weight: 600; }
  .up-choose { padding: 0; border: 0; background: none; font: inherit; color: var(--accent);
    text-decoration: underline; cursor: pointer; }
  .up-or { display: flex; align-items: center; gap: 9px; margin: 11px 0 9px;
    color: var(--text-3); font-size: 0.67rem; }
  .up-or::before, .up-or::after { content: ""; height: 1px; flex: 1; background: var(--glass-line); }
  /* Who the alias credited, on the card. Quiet when it worked and quiet when
     it did not — this is a fact about the row, not a warning. */
  .up-credit { margin: 2px 0 0; font-size: 0.7rem; line-height: 1.4; }
  .up-credit.is-ok { color: var(--text-3); }
  .up-credit.is-note { color: var(--warn, #f5a524); }
  .up-link-row { display: flex; gap: 7px; }

  /* Inputs are pressed INTO the glass rather than raised off it — a lighter
     fill on a frosted panel reads as another pane stacked on top. */
  .up-win input[type=text], .up-win input[type=url], .up-win textarea {
    background: var(--glass-sunk); border: 1px solid var(--glass-line); color: var(--text);
    font-family: inherit; border-radius: 9px; }
  .up-win input:focus, .up-win textarea:focus { outline: none; border-color: var(--accent); }
  .up-win input::placeholder, .up-win textarea::placeholder { color: var(--text-3); }
  .up-link-row input { flex: 1; min-width: 0; font-size: 0.79rem; padding: 9px 11px; }

  /* Limits before you spend time, never as a rejection afterwards. */
  .up-limits { margin-top: 10px; font-family: var(--font-mono); font-size: 0.61rem;
    color: var(--text-3); text-align: center; line-height: 1.8; }

  .up-qh { display: flex; align-items: center; gap: 8px; margin: 17px 0 8px; }
  .up-qh span { font-family: var(--font-mono); font-size: 0.63rem; letter-spacing: 0.05em;
    color: var(--text-3); text-transform: uppercase; }
  .up-qh::after { content: ""; flex: 1; height: 1px; background: var(--glass-line); }
  .up-q { display: flex; flex-direction: column; gap: 8px; }
  .up-empty { text-align: center; color: var(--text-3); font-size: 0.73rem; padding: 12px 0 2px; }

  /* Cards ON glass are lighter glass, never solid — an opaque box inside a
     frosted panel reads as a hole punched through it. */
  .up-item { background: var(--glass-2); border: 1px solid rgba(255,255,255,0.055);
    border-radius: 11px; padding: 9px 10px; animation: rise 0.26s ease both; }
  .up-top { display: flex; gap: 9px; align-items: center; }
  .up-thumb { width: 44px; height: 29px; flex: none; border-radius: 6px;
    background: var(--surface); display: grid; place-items: center; color: var(--text-3);
    overflow: hidden; }
  .up-thumb img { width: 100%; height: 100%; object-fit: cover; }
  .up-mid { flex: 1; min-width: 0; }
  .up-name { font-size: 0.78rem; font-weight: 600; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  /* Lowercase, verbs, no exclamation. The state line is the whole voice. */
  .up-state { font-size: 0.68rem; color: var(--text-3); margin-top: 1px; }
  .up-ib { background: none; border: 0; color: var(--text-3); width: 22px; height: 22px;
    border-radius: 6px; flex: none; display: grid; place-items: center; cursor: pointer; }
  .up-ib:hover { background: var(--glass-3); color: var(--text); }
  .up-chev { transition: transform 0.2s ease; }
  .up-item.shut .up-chev { transform: rotate(-90deg); }
  .up-item.shut .up-form { display: none; }

  /* A hairline, not a loading bar. */
  .up-bar { height: 2px; margin-top: 8px; border-radius: 2px;
    background: rgba(255,255,255,0.08); overflow: hidden; }
  .up-bar i { display: block; height: 100%; width: 0; border-radius: 2px;
    background: var(--accent); transition: width 0.3s linear; }
  .up-bar.indet i { width: 34%; animation: up-sweep 1.15s ease-in-out infinite;
    background: var(--text-3); }
  @keyframes up-sweep { 0% { margin-left: -34%; } 100% { margin-left: 100%; } }
  .up-item.done .up-bar { display: none; }
  .up-item.sad { border-color: color-mix(in srgb, var(--tl-not) 34%, transparent); }
  .up-item.sad .up-state { color: var(--tl-not); }
  .up-item.warn { border-color: color-mix(in srgb, var(--tl-meta) 32%, transparent); }
  .up-item.warn .up-state { color: var(--tl-meta); }

  /* Open from the moment the item appears, because the upload and the typing
     are the same minute. Filling this in while bytes move is free; asking for
     it afterwards is a second visit. */
  .up-form { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--glass-line); }
  .up-lb { font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--text-3); margin-bottom: 4px; display: block; }
  .up-f { margin-bottom: 10px; }
  .up-f input[type=text], .up-f textarea { width: 100%; font-size: 0.78rem; padding: 7px 9px; }
  .up-f textarea { resize: vertical; min-height: 62px; line-height: 1.55;
    font-family: var(--font-mono); font-size: 0.72rem; }

  /* Chips in, autocomplete inline rather than floating — a dropdown positioned
     inside a scrolling panel gets clipped by it, and pushing the form down for
     a moment costs nothing. */
  .up-tagbox { display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
    background: var(--glass-sunk); border: 1px solid var(--glass-line);
    border-radius: 9px; padding: 5px 6px; min-height: 32px; }
  .up-tagbox:focus-within { border-color: var(--accent); }
  .up-tagbox.has-bad { border-color: color-mix(in srgb, var(--tl-not) 62%, transparent); }
  .up-tagbox input { flex: 1; min-width: 74px; background: none !important;
    border: 0 !important; outline: none; color: var(--text); font: inherit;
    font-size: 0.75rem; padding: 2px; }
  /* All six kinds, and a neutral default above them.
     This mapped four, and an unresolvable var() invalidates the whole
     declaration — so a `type` or `elements` chip got neither its background
     nor its colour and came out as bare text on the box. Same trap as the
     blur() one; --seg- is the ramp that has all six. */
  .up-chip { display: inline-flex; align-items: center; gap: 3px;
    font-family: var(--font-mono); font-size: 0.63rem; padding: 2px 3px 2px 7px;
    border-radius: 6px; --tl: var(--text-3);
    background: color-mix(in srgb, var(--tl) 20%, transparent);
    color: var(--tl); }
  .up-chip[data-kind="copyright"] { --tl: var(--seg-copyright); }
  .up-chip[data-kind="character"] { --tl: var(--seg-character); }
  .up-chip[data-kind="type"]      { --tl: var(--seg-type); }
  .up-chip[data-kind="elements"]  { --tl: var(--seg-elements); }
  .up-chip[data-kind="meta"]      { --tl: var(--seg-meta); }
  .up-chip[data-kind="artist"]    { --tl: var(--seg-artist); }
  .up-chip[data-kind="general"]   { --tl: var(--seg-general); }
  /* Not in the vocabulary. Red, struck through, and it blocks the submit —
     submitters attach tags that exist, they do not mint new ones, and the
     cheapest place to say so is the moment it is typed. */
  /* Dashed and grey rather than struck through and red. It used to mean
     "this will be refused"; it now means "the archive does not have this yet",
     and those want opposite colours. */
  .up-chip.new { background: none; border: 1px dashed var(--glass-line);
    color: var(--text-3); padding: 1px 2px 1px 6px; }
  .up-chip.new::before { content: "+"; opacity: 0.7; margin-right: 2px; }
  /* Suggested, not yet approved. It keeps the kind colour it will keep for
     good — the category was chosen at mint time and is not in doubt — at a
     lighter wash, inside a dashed edge.
     Deliberately NOT `.new`'s grey. `.new` means the archive has never heard
     of this name and it is riding along as loose text; `pend` means the
     archive HAS it, from this person, and an editor has not looked yet. Two
     different sentences, and collapsing them would be the third time this
     field told somebody their tag was one thing when it was another. */
  .up-chip.pend { background: color-mix(in srgb, var(--tl) 9%, transparent);
    border: 1px dashed color-mix(in srgb, var(--tl) 55%, transparent);
    padding: 1px 2px 1px 6px; }
  .up-chip button { background: none; border: 0; color: inherit; opacity: 0.7;
    font-size: 0.78rem; line-height: 1; padding: 0 2px; cursor: pointer; }
  .up-chip button:hover { opacity: 1; }

  .up-ac { margin-top: 5px; border: 1px solid var(--glass-line); border-radius: 9px;
    background: rgba(0,0,0,0.2); overflow: hidden; }
  .up-ac-row { display: flex; align-items: center; gap: 8px; width: 100%; border: 0;
    background: none; color: var(--text); font: inherit; font-size: 0.74rem;
    padding: 6px 9px; text-align: left; cursor: pointer; --tl: var(--text-3);
    box-shadow: inset 3px 0 0 var(--tl); }
  .up-ac-row[data-kind="copyright"] { --tl: var(--seg-copyright); }
  .up-ac-row[data-kind="character"] { --tl: var(--seg-character); }
  .up-ac-row[data-kind="type"]      { --tl: var(--seg-type); }
  .up-ac-row[data-kind="elements"]  { --tl: var(--seg-elements); }
  .up-ac-row[data-kind="meta"]      { --tl: var(--seg-meta); }
  .up-ac-row[data-kind="artist"]    { --tl: var(--seg-artist); }
  .up-ac-row[data-kind="general"]   { --tl: var(--seg-general); }
  /* Last in the list and never the consequence of not picking something —
     the same rule the pickers follow, for the same reason: typing "sca" where
     `scared` already exists should not quietly make a second one. */
  .up-ac-row.mint { --tl: var(--accent); color: var(--accent); }
  /* The same row, waiting. The lit left edge goes dashed — a solid one is the
     page's mark for "this is vocabulary" everywhere else, so it is the right
     thing to interrupt. */
  .up-ac-row.pend { box-shadow: none;
    border-left: 3px dashed color-mix(in srgb, var(--tl) 70%, transparent); }
  .up-ac-row.pend .k { color: var(--tl); opacity: 0.85; }
  .up-ac-row.at, .up-ac-row:hover { background: color-mix(in srgb, var(--tl) 26%, transparent); }
  .up-ac-row b { color: var(--tl); font-weight: 700; }
  .up-ac-row .k { margin-left: auto; font-size: 0.62rem; color: var(--text-3); }
  /* `pop2-kinds` with the picker's art-column indent taken off. One chip row
     in the page for one decision, rather than a third look for it.
     Two classes deep on purpose: `.pop2-kinds` is declared further down the
     sheet, so at equal specificity it would win the padding back. */
  .up-ac .up-ac-kinds { padding: 1px 9px 8px; }
  /* Grey, not red, and it says what to do. Finding no match used to mean the
     name would be refused; it now means the archive has not got it yet, which
     is an invitation rather than a failure. */
  .up-ac-none { padding: 6px 9px; font-size: 0.7rem; color: var(--text-3); }
  .up-ac-none b { color: var(--text-2); font-weight: 700; }

  /* On by default: whisper is the normal path and the field below is for the
     rarer case where somebody already has the words. */
  .up-sw-row { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
  /* A qualifier on a label, so quieter than the label. */
  .up-when { color: var(--text-3); opacity: 0.75; }
  .up-sw-row .up-lb { margin: 0; flex: 1; }
  .up-sw { width: 32px; height: 18px; border-radius: 20px; border: 0; padding: 2px;
    background: var(--surface-3); display: flex; cursor: pointer;
    transition: background 0.16s ease; }
  .up-sw i { width: 14px; height: 14px; border-radius: 50%; background: var(--text-3);
    transition: transform 0.16s cubic-bezier(0.22, 1, 0.36, 1), background 0.16s ease; }
  .up-sw[aria-checked="true"] { background: color-mix(in srgb, var(--accent) 46%, transparent); }
  .up-sw[aria-checked="true"] i { transform: translateX(14px); background: var(--accent); }
  .up-note2 { font-size: 0.68rem; color: var(--text-3); line-height: 1.5; }

  .up-foot2 { display: flex; align-items: center; gap: 9px; margin-top: 2px; }
  .up-why { flex: 1; font-size: 0.67rem; color: var(--text-3); min-width: 0; }
  .up-why.bad { color: var(--tl-not); }
  .up-send { border: 0; border-radius: 8px; padding: 7px 12px; font-size: 0.74rem;
    font-weight: 700; background: var(--accent); color: var(--on-accent); flex: none;
    cursor: pointer; }
  .up-send[disabled] { background: var(--glass-2); color: var(--text-3); cursor: not-allowed; }

  /* The exact-duplicate note. Amber, not red: nothing went wrong, the archive
     simply already has these bytes. */
  .up-dupe { margin-top: 9px; padding: 9px; border-radius: 9px;
    background: color-mix(in srgb, var(--tl-meta) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--tl-meta) 26%, transparent);
    font-size: 0.73rem; color: var(--text-2); }
  .up-dupe b { color: var(--tl-meta); font-weight: 600; }

  /* The near-duplicate note, which makes a DIFFERENT CLAIM from the one above
     and has to look like one. Byte-for-byte identical is a fact and the end of
     that upload; this is a resemblance, and the row it sits on is alive — the
     bytes landed, the form is open, nothing went wrong. So it is a block
     BESIDE the form rather than one that replaces it. */
  .up-near { margin-top: 9px; padding: 9px 9px 7px; border-radius: 9px;
    background: color-mix(in srgb, var(--tl-meta) 8%, transparent);
    border: 1px solid color-mix(in srgb, var(--tl-meta) 22%, transparent);
    font-size: 0.73rem; color: var(--text-2); }
  .up-near-t { color: var(--text); font-weight: 600; margin-bottom: 2px; }
  .up-match { display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
    padding: 6px 0; border-top: 1px solid rgba(255, 255, 255, 0.06); }
  .up-match:first-of-type { border-top: 0; }
  .up-match .up-choose { margin-left: auto; }
  /* Ours, for comparison, and it is the whole reason the sizes are here: the
     question somebody is about to answer is which of the two to keep. */
  .up-mine { padding-top: 6px; border-top: 1px dashed rgba(255, 255, 255, 0.09); }
  /* THE BAND, and it is ONE VOCABULARY IN THREE PLACES: the upload card, a
     queue row, and a gallery tile. Two words, and they are two different
     claims — four bits is where a re-encode and a disagreeing scaler land
     (`phashcross` measures it), so inside that the two files are the same
     picture and outside it they merely look alike. One colour for both would
     be a lie about which one you are reading.
     THE FACE IS HERE so a fourth place cannot invent a second red; geometry
     belongs to whichever member it is sitting in. */
  .dupband { flex: none; padding: 1px 6px; border-radius: 5px; font-size: 0.6rem;
    font-weight: 700; letter-spacing: 0.06em; }
  .dupband.is-same { color: var(--tl-not);
    background: color-mix(in srgb, var(--tl-not) 16%, transparent);
    border: 1px solid color-mix(in srgb, var(--tl-not) 38%, transparent); }
  /* THE THIRD ANSWER. `IDENTICAL` and `SIMILAR` are both about how alike two
     pictures LOOK; this one is about a fact — the same picture at two sizes —
     and it is the one of the three that is almost always a copy somebody did
     not mean to keep. Louder than `SIMILAR` and quieter than a refusal. */
  .dupband.is-resized { color: var(--tl-hold);
    background: color-mix(in srgb, var(--tl-hold) 16%, transparent);
    border: 1px solid color-mix(in srgb, var(--tl-hold) 36%, transparent); }
  .dupband.is-near { color: var(--tl-meta);
    background: color-mix(in srgb, var(--tl-meta) 16%, transparent);
    border: 1px solid color-mix(in srgb, var(--tl-meta) 38%, transparent); }
  .up-dim { color: var(--text-3); font-variant-numeric: tabular-nums; }

  /* ---- the upload window, docked ----------------------------------------
     The floating card spent height on four edges, a radius and a big soft
     shadow, all of which exist to say "this is a separate thing from the
     page". The right edge of the screen says that for free: three of the four
     edges go, the 78vh ceiling stops being a budget, and the drag goes with
     them -- there is nowhere to drag a drawer to.

     IT STARTS UNDER THE SITE BAR. `.topbar` is sticky at 0 and 64 tall and
     holds the tabs; a drawer over it hides the control you press to leave the
     view, and z-index 92 means it would win. `--up-top` has to track
     `.topbar-inner`'s height.

     A CLASS AND NOT `.up-win`, because three windows wear that: this one, the
     API-keys window and Add-a-song. Which of the others should dock is a
     decision, not a consequence. */
  /* Measured, not guessed: `.topbar-inner` is a fixed 64 and `.tabbar` comes
     out 49 at every width (the tabs do not wrap). Both are declared here so
     the drawer and the sticky stack cannot disagree about where the page
     begins -- if either bar changes height, this is the one line to change. */
  :root { --topbar-h: 64px; --tabbar-h: 49px;
          --up-top: calc(var(--topbar-h) + var(--tabbar-h));
          --up-w: 468px; }
  .up-win.up-side {
    top: var(--up-top); right: 0; bottom: 0; left: auto;
    width: var(--up-w);
    height: auto; max-height: none;
    border-radius: 0;
    border-width: 0 0 0 1px;
    /* THE LIT EDGE MOVES. `.glass` lights the TOP with
       `inset 0 1px 0 0 var(--glass-hi)`, which is right for a card lying on
       the page and wrong for a pane standing against its right edge. The drop
       shadow turns the same corner -- a card casts down, a drawer casts in. */
    box-shadow: -26px 0 64px rgba(0,0,0,.52), -3px 0 10px rgba(0,0,0,.34),
                inset 1px 0 0 0 var(--glass-hi);
    /* A slide and nothing else: the card faded in because it appeared out of
       nowhere under a button, and a drawer comes from a known place. 0.26s
       rather than the card's 0.19 -- that travelled 8px and this travels 468. */
    opacity: 1;
    transform: translateX(100%);
    transform-origin: right center;
    transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
  }
  /* RESTATED, and it has to be: `.up-win.open { transform: none }` is declared
     above at the same specificity, so the translate would win on later-wins
     and the drawer would never come in. */
  .up-win.up-side.open { transform: none; }
  @media (prefers-reduced-motion: reduce) { .up-win.up-side { transition: none; } }
  /* The rake turns with the panel: a tall pane lit from its left edge. */
  .up-win.up-side::before {
    background: linear-gradient(90deg, rgba(255,255,255,0.05) 0%,
                                       rgba(255,255,255,0) 34%); }
  /* Nothing to grab, and one row rather than two: the subtitle said the same
     sentence every time and the title is already on the button you pressed.
     The approval line joined `.up-limits`, which is the rules line. */
  .up-win.up-side .up-head { cursor: default; align-items: center; padding: 13px 16px; }
  .up-win.up-side .up-body { padding: 0 16px 18px; }

  /* ---- and it comes out from under the bars ----------------------------
     The drawer starts below both of them, so nothing of it is actually behind
     the tab bar -- but its shadow is a 64px blur with no offset, and that
     spills up across the tabs and reads as a panel lying ON them. Raising the
     two bars over it puts the shadow where it belongs: under the edge the
     drawer appears to slide out from.

     ONLY WHILE IT IS OPEN, because these numbers are load-bearing elsewhere:
     `.mu-modal` is a full-screen 90 that is meant to cover the bars, and the
     taglet pickers are 96 and must stay over everything. Shut, the stack is
     exactly what it was. */
  body.up-open .topbar { z-index: 93; }
  body.up-open .tabbar { z-index: 93; }

  /* ---- and the bar that opens it keeps its button ----------------------
     The control is at the right end of a full-width toolbar and the drawer is
     pinned to the right edge, so docking put the open/close button underneath
     it. Found by `upclose`, which could no longer press it a second time.

     THE BAR MOVES AND THE PAGE DOES NOT, which is the whole reason the drawer
     sits over rather than beside: `.toolbar` is a flex row whose search box is
     `flex: 1 1 240px`, so it gets narrower and nothing below it reflows. */
  body.up-open .toolbar { padding-right: var(--up-w); }
  .toolbar { transition: padding-right 0.26s cubic-bezier(0.22, 1, 0.36, 1); }

  /* ---- the drop zone, out of the scroll --------------------------------
     A full-height drawer holds a long queue, and the thing you came to do --
     put another file in -- was at the top of it, scrolled away. So the drop
     block leaves `.up-body` and sits between the head and the scroller.
     Nothing sticky is involved: a sticky child inside a backdrop-filtered
     parent needs an opaque backing of its own, which reads as a seam. */
  .up-win.up-side .up-fix { padding: 13px 16px 12px; flex: none; }
  /* THE CHANNEL POPOVER ANCHORS TO THE BLOCK, not to the word. `.up-chanwrap`
     is inline and `left: 0` means the anchor's left edge — which in a centred
     sentence inside a drawer flush with the screen's right edge put a 19rem
     list off the side of the page. Anchored to `.up-limits` it has the
     window's own width to open in. */
  .up-win.up-side .up-limits { position: relative; }
  .up-win.up-side .up-chanwrap { position: static; }
  .up-win.up-side .up-fix .up-limits { margin-top: 9px; }
  /* AND IT SHRINKS ONCE THERE IS A QUEUE. The icon and the two-line
     invitation are for an empty window; with cards below it the invitation
     has been accepted and the field is all that is still wanted. The dashed
     edge stays -- it is the only thing saying a file may be dropped in here. */
  .up-win.up-side.has-q .up-fix { border-bottom: 1px solid var(--glass-line); }
  .up-win.up-side.has-q .up-drop { padding: 9px 10px; text-align: left; }
  .up-win.up-side.has-q .up-drop-i,
  .up-win.up-side.has-q .up-drop-t,
  .up-win.up-side.has-q .up-or { display: none; }
  /* THE RULES STAY, and that is not a hedge. They were in the hidden set, and
     the channel popover is anchored to a word in the middle of them — so the
     answer to "may I paste this Discord link" disappeared the moment there
     was one upload in flight, which is exactly when somebody is pasting the
     second. `picform` caught it by not being able to press the button. */
  .up-win.up-side.has-q .up-fix .up-limits { margin-top: 7px; line-height: 1.6; }
  .up-win.up-side.has-q .up-more { display: flex; }
  .up-more { display: none; align-items: center; gap: 8px; margin-bottom: 7px;
    font-size: 0.74rem; color: var(--text-3); }
  .up-more svg { flex: none; }
  .up-more .up-choose { margin-left: auto; font-size: 0.72rem; }
  /* A reader who may only paste a link is not being offered a drop. */
  .up-win[data-files="0"] .up-more svg,
  .up-win[data-files="0"] .up-more .up-choose { display: none; }

  /* ---- and an empty one is a box, not a column -------------------------
     A full-height dashed box was the first answer and it was too much of a
     good thing: the drop target only needs to be a target, and a column-high
     one leaves the window looking like it has nothing else in it. 240px fits
     the icon, the invitation, the rule and the link row with air around them;
     below it the queue says what it always said, which is that it is empty. */
  .up-win.up-side:not(.has-q) .up-drop { min-height: 240px; display: flex;
    flex-direction: column; justify-content: center; }

  /* ---- the card -------------------------------------------------------
     SAVE MOVES UP INTO THE CARD'S OWN ROW, where the name and the state
     already are: a tall drawer stacks several cards, and a verb at the foot
     of each one is a verb somebody has to scroll a form to find. What is left
     at the foot is the reason a save cannot go, which is usually nothing --
     an empty 28px row under every card. */
  .up-win.up-side .up-top { align-items: center; }
  .up-win.up-side .up-send { flex: none; }
  .up-win.up-side .up-foot2:has(.up-why:empty) { display: none; }

  /* ---- description and pool on one row ---------------------------------
     Two full-width rows for one sentence and one short code. The field is
     8ch and its text may scroll: a pool id is pasted and read back, never
     composed. `maxlength` stays 26 because `readPoolId` takes eight
     characters OR a whole ULID, and `upMintPool` still mints the ULID. */
  .up-win.up-side .up-row { display: flex; gap: 6px; align-items: center; }
  .up-win.up-side .up-row > input[type=text] { flex: 1 1 auto; min-width: 0;
    font-size: 0.78rem; padding: 7px 9px; }
  .up-win.up-side .up-pool { flex: 0 0 auto; width: 8ch;
    font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase; }
  /* The ids are uppercase; the hint is not a value. */
  .up-win.up-side .up-pool::placeholder { text-transform: none; }
  /* `rv-ico` is a 38px box for a toolbar; in a field row it has to be the
     height of the field beside it, or it is 28.4-beside-30 again. */
  .up-win.up-side .up-row .rv-ico { width: 32px; height: 32px; border-radius: 9px; }

  /* ON A TILE: bottom-right, which is the one corner the grid leaves free —
     the tick is top-left, the operations top-right, the sequence mark
     bottom-left. Visible at rest rather than on hover, because it is a FACT
     about the picture and not an action; the operations are gated, this is
     not. And it is a LINK, because the thing you want on seeing it is the
     other picture. */
  /* BOTTOM LEFT, which it was not: this sat bottom-right, where the count on
     a collapsed sequence now goes. A deck with a stray near it wants both at
     once, and two pills in one corner is two pills nobody reads. */
  .pic-dup { position: absolute; left: 5px; bottom: 5px; z-index: 7;
    text-decoration: none; backdrop-filter: blur(7px); }

  /* IN A QUEUE ROW: the same shape as `.snip-sup` above it, deliberately.
     That one says "this replaces that" and this one says "this looks like
     that" — the weaker claim of the pair, and a reviewer reads them as one
     family or has to learn two. */
  .snip-dup { display: flex; align-items: center; gap: 8px; margin: 0 0 7px;
    padding: 5px 7px; border-radius: 8px; background: var(--surface-2);
    border: 1px solid var(--glass-line); font-size: 0.7rem; color: var(--text-3); }
  .snip-dup img { width: 34px; height: 34px; flex: none; border-radius: 5px;
    object-fit: cover; background: var(--surface-3); }
  .snip-dup a { margin-left: auto; flex: none; color: var(--accent); }
  /* The override, said plainly rather than drawn as an icon: it is the one
     control here that HIDES something from everybody else, and a glyph would
     make it guessable rather than readable. */
  .snip-dup .rv-plain { flex: none; padding: 2px 7px; border-radius: 6px;
    font: inherit; font-size: 0.68rem; cursor: pointer; color: var(--text-2);
    background: var(--surface-3); border: 1px solid var(--glass-line); }
  .snip-dup .rv-plain:hover { color: var(--text); border-color: var(--text-3); }
  /* Asked, and waiting for the second press. */
  .snip-dup .rv-plain-ask { color: var(--tl-hold); border-color: var(--tl-hold); }

  /* ── TWO PICTURES, LOOKED AT PROPERLY ─────────────────────────────────────
     The mark in a queue row is 34 pixels square, which is enough to say THAT
     two pictures are alike and nowhere near enough to decide WHETHER they are
     the same one. This is where that decision is made, and the verbs are in
     here with it so it is made once.
     A FIXED OVERLAY AND NOT A NEW TAB. `look at it` stays, because sometimes
     the full file is what you want — but a tab loses the pair, which is the
     only thing being judged. */
  /* ══ THE PICTURE, LARGE ═════════════════════════════════════════════════
     Body is a two-column grid and the side column is `0fr` until there is
     something in it, so a picture with no collision opens as one big picture
     and nothing is reserved for an absence. */
  /* ONE ROW, AND IT IS `1fr` RATHER THAN `auto`. That is what gives the panes
     inside it a definite height to divide, and without it `flex: 1` on a
     picture box means "as tall as the picture" rather than "as tall as what is
     left" — the row grows, and everything below the stage is pushed off the
     bottom of the window. */
  .dcm-body { display: grid; grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: minmax(0, 1fr);
    gap: 18px; align-items: stretch; min-height: 0; }
  .dcm-main { display: flex; flex-direction: column; gap: 10px; min-width: 0;
    min-height: 0; }
  #dcm-shot { display: grid; place-items: center; min-height: 0;
    background: #06050a; border-radius: 12px; overflow: hidden;
    border: 1px solid var(--surface-3); }
  #dcm-shot { flex: 1; min-height: 0; grid-template-rows: minmax(0, 1fr); }
  #dcm-shot img { max-width: 100%; max-height: 100%;
    object-fit: contain; display: block; }
  /* CENTRED, under the picture they are about. They were left-aligned in a
     box as wide as the viewport, so on a wide screen the facts sat in one
     far corner and the REVISE words in the other — and answering a word meant
     crossing the whole screen from the picture to get to it. */
  .dcm-facts { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
    justify-content: center;
    font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-3); }
  .dcm-facts b { color: var(--text-2); font-weight: 600; }
  .dcm-facts .is-live { color: var(--tl-general); }
  /* The bigger of the two, said in the one place somebody is looking when
     they ask which to keep. */
  .dcm-facts .dcm-big, .dcm-dim .dcm-big { color: var(--tl-general); }
  /* A DOT AND NOT A GAP between the three measurements, because they are one
     fact about one file and the status beside them is a different fact. */
  .dcm-dot { font-style: normal; color: var(--text-4, var(--text-3)); opacity: 0.6;
    margin: 0 -2px; }
  .dcm-seq { font-family: var(--font-mono); color: var(--text-2);
    background: var(--surface-2); border-radius: 5px; padding: 0 5px; }
  .dcm-sep { width: 1px; align-self: stretch; margin: 0 2px;
    background: var(--glass-line); }

  /* The same block the card carries, in the same red, because it is the same
     fact and a second face for it would be a second thing to learn. */
  .dcm-fix { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
    justify-content: center; font-size: 0.74rem; }
  /*  The card's block has a margin under it for the row beneath; here it is
      the last thing on the surface and the margin is a gap to nothing. */
  .dcm-fix .snip-fix { margin: 0; }
  .dcm-fix:empty { display: none; }
  /* ── THE BAND OF VERBS ───────────────────────────────────────────────
     Centred, because it is about the picture in the middle and not about the
     card on the right — and two rows, because the verdict and the collision
     are two questions that happen to be asked at the same moment. */
  .dcm-acts { flex: none; display: flex; flex-direction: column; align-items: center;
    gap: 8px; padding: 10px 14px 4px; }
  .dcm-acts:empty { display: none; }
  .dcm-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
    justify-content: center; }
  .dcm-row.is-paths .btn-op { font-size: 0.74rem; }
  .dcm-row .btn-primary { min-width: 108px; justify-content: center; }

  /* ── AND THE COLLISION, SIDE BY SIDE AND THE SAME SIZE ───────────────────
     A CARD WAS THE WRONG SHAPE, AND THE FLIP WAS CARRYING IT. The twin sat in
     a 272-pixel column at `object-fit: cover` — so it was both small and
     cropped, and the only way to see the whole of it at a size worth judging
     was to hold F and swap it into the middle. That turns a comparison into a
     thing done from memory, one picture at a time, which is most of why this
     surface was reported as unpleasant to use: *"the controls are a little
     whack to use and just comparing images with the eyes is a pain"*, and
     then, exactly: *"it would be better to have them side by side properly"*.

     SO TWO PANES OF EQUAL WIDTH, both `contain`, both bounded by the same
     height. That is the whole of "properly": two pictures at ONE size means
     one `max-height` shared by both boxes, not two that happen to look alike.

     THE FLIP STAYS AND IS NO LONGER LOAD-BEARING. Side by side answers "is
     this the same composition", which is the question nine times in ten.
     Alternating two pictures inside one fixed box is still the only thing
     that answers "is this the same FILE" to within a few pixels, because the
     eye cannot hold two images at that precision — so F now SWAPS the panes
     rather than hiding one of them, and both pictures are on screen either
     way.

     AND THE RING SAYS WHICH ONE IS BEING DECIDED. Every verb on the band acts
     on `DCM.a`; with two pictures at one size there was nothing in the layout
     that said which of them that is. Asked for in those words — *"the image
     being decided on being marked with a ring selector to emphasize that this
     is the one being decided on"* — and it is the cheapest kind of fix there
     is, because the alternative to reading it off the layout is giving a
     verdict to the wrong half. */
  /* `stretch` AND NOT `start`, WHICH IS WHAT MAKES THE BOUND SHARED. Both
     panes are full-height grid items, both put their picture in a box that
     takes the slack, and both put their facts under it — so the two boxes are
     the same height because they are the same computation, not because two
     numbers happen to agree. On a tall window the pictures grow into it
     instead of leaving a hand's width of nothing above the verbs. */
  .dcm-stage { display: grid;
    grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
    grid-template-rows: minmax(0, 1fr);
    gap: 14px; align-items: stretch; flex: 1; min-height: 0; }
  .dcm-pane { display: flex; flex-direction: column; gap: 8px; min-width: 0;
    min-height: 0; }
  .dcm-side { min-width: 0; display: flex; flex-direction: column; gap: 8px; }
  .dcm-side:empty { display: none; }
  .dcm-twin { border: 1px solid var(--surface-3); border-radius: 12px;
    background: #06050a; overflow: hidden; display: grid; place-items: center;
    grid-template-rows: minmax(0, 1fr); flex: 1; min-height: 0; }
  /* THE SAME BOUND AS `#dcm-shot`, and `contain` rather than `cover`: a crop
     of a composition is not the composition, and a cropped thumbnail is
     exactly how a variant and a resize come to look identical. */
  .dcm-twin img { max-width: 100%; max-height: 100%;
    object-fit: contain; display: block; cursor: zoom-in; }
  /* ── THE SAME SHAPE AS `.dcm-facts`, WHICH IS THE POINT OF IT ───────────
     A wrapped row and not a column. As a column it was four lines tall against
     the subject's one, and a grid item whose footer is three lines taller gets
     a picture three lines shorter — so the two boxes stopped being the same
     size, which is the single thing this layout exists to guarantee. */
  .dcm-twin-b { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
    justify-content: center; font-family: var(--font-mono); font-size: 0.7rem;
    color: var(--text-3); }
  .dcm-where { font-size: 0.74rem; color: var(--text-2); }
  .dcm-where b { color: var(--text); font-weight: 650; }
  .dcm-dim { font-family: var(--font-mono); font-size: 0.66rem; color: var(--text-3);
    display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
    justify-content: center; }
  /* ── THE RING, ON THE PANE AND NOT ON THE PICTURE ────────────────────────
     So it reads as "this COLUMN is the subject" rather than as a border the
     picture happens to have: a 1px frame on a dark photograph is invisible,
     and a 2px one on a pale one looks like part of the file. Drawn on
     whichever pane currently holds `DCM.a`, which is why the class moves with
     the flip instead of sitting in the markup. */
  .dcm-pane.is-subject > #dcm-shot,
  .dcm-side.is-subject .dcm-twin { box-shadow: 0 0 0 2px var(--accent);
    border-color: transparent; }
  .dcm-lab { font-family: var(--font-mono); font-size: 0.6rem;
    letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }
  .dcm-pane.is-subject .dcm-lab, .dcm-side.is-subject .dcm-lab,
  .line-pane.is-at .dcm-lab { color: var(--accent); }
  /* WHO MADE IT, WHICH IS THE ONE FACT THE TAG DIFF COULD NOT BE READ FOR.
     See `dcmBy`: the diff prints bare words, so an artist is one chip among
     six and indistinguishable from a character. An unmapped booru handle is
     drawn as a question because it is not a tag yet — it is the thing being
     decided, and reading it as an answer is the mistake. */
  .dcm-by { font-size: 0.7rem; color: var(--text-2); }
  .dcm-by b { color: var(--tl-artist, var(--text)); font-weight: 600; }
  .dcm-by i { font-style: normal; color: var(--text-3); }
  .dcm-by.is-none { color: var(--text-3); opacity: 0.75; }
  /* ── THE TAGS, PRESSABLE, WHERE THE PICTURE IS LARGE ─────────────────────
     `snipStrip` itself and not a second face for it — the chips, the × that
     detaches, the suggestion chips and `+ tag`, exactly as the card draws
     them. It carries `rv` because `snipRepaint` reads that class to decide
     whether to rebuild the strip with a reviewer's flags, and this surface is
     a reviewer's. */
  .dcm-strip { display: flex; flex-wrap: wrap; gap: 5px; align-items: center;
    justify-content: center; padding: 0 14px 2px; }
  .dcm-strip:empty { display: none; }
  /* ── THE REVERSE SEARCH, AND IT IS A ROW OF LEADS ───────────────────────
     Horizontal and scrolling rather than a list: eight results of which one
     matters, and the one that matters is the leftmost because they are sorted
     by similarity. A vertical list of eight would push the picture off the
     surface the search was started from.
     THE THUMBNAILS ARE SAUCENAO'S OWN and are loaded from their host, which
     is the one place in this page that happens. `referrerpolicy` because an
     outside host does not need to be told which page of a private archive is
     looking at it, and a broken thumbnail is a missing picture rather than a
     broken layout. */
  .dcm-sauce { display: flex; gap: 8px; align-items: stretch; overflow-x: auto;
    padding: 2px 14px 8px; }
  .dcm-sauce:empty { display: none; }
  .dcm-sauce .sn-say { font-size: 0.72rem; color: var(--text-3);
    align-self: center; }
  .dcm-sauce .sn-say.is-bad { color: #ffb3b3; }
  .dcm-hit { flex: none; width: 168px; display: flex; flex-direction: column;
    gap: 3px; border: 1px solid var(--surface-3); border-radius: 10px;
    padding: 6px; background: var(--surface); }
  /* THE SIMILARITY IS THE HEADLINE because it is the only number that decides
     whether to read the rest. Over eighty is almost always the same picture;
     under sixty almost never is, and the colour says so before the digits
     are read. */
  .dcm-hit b { font-family: var(--font-mono); font-size: 0.74rem;
    color: var(--text-3); }
  .dcm-hit.is-strong b { color: var(--tl-general); }
  .dcm-hit.is-weak { opacity: 0.6; }
  .dcm-hit img { width: 100%; height: 86px; object-fit: cover; border-radius: 6px;
    display: block; background: #06050a; }
  .dcm-hit .sn-site { font-size: 0.64rem; color: var(--text-3);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* THE ARTIST IN THE ARTIST COLOUR, which is the whole reason the button
     exists: an unknown handle on the card, and a name here to check it
     against. Same swatch the chips wear, so it reads as the same kind of
     thing. */
  .dcm-hit .sn-by { font-size: 0.7rem; color: var(--tl-artist);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .dcm-hit .sn-links { display: flex; flex-wrap: wrap; gap: 6px;
    font-family: var(--font-mono); font-size: 0.62rem; }
  /* A HUE OFF THE POOL ID, the same one the queue's own badge wears — so "these
     two are already together" is something seen rather than eight characters
     compared by eye. See `poolHue`, which argues this at length. */
  .dcm-seq[style*="--pool-hue"], .line-pool { padding: 1px 6px; border-radius: 5px;
    background: hsl(var(--pool-hue, 0) 60% 50% / 0.2);
    box-shadow: inset 0 0 0 1px hsl(var(--pool-hue, 0) 70% 62% / 0.45); }
  /* ── AND THE KIND, WITHOUT WHICH THE DIFF IS UNREADABLE ──────────────────
     The chips were bare words in one colour, so of six words under `only left`
     there was no way to tell the artist from a character or a copyright — and
     the case this block exists to serve is reading one artist's name against
     the one the archive already believes. Reported as exactly that: `furiyuri`
     seen beside a remembered `furuyuri`, with no way to find out which is
     which. The same swatches the chips wear everywhere else, so a colour means
     here what it means on a card. */
  .dcm-tags .dcm-only[data-kind] { color: var(--tl, inherit); }
  .dcm-tags .dcm-only[data-kind="character"] { --tl: var(--tl-character); }
  .dcm-tags .dcm-only[data-kind="copyright"] { --tl: var(--seg-copyright); }
  .dcm-tags .dcm-only[data-kind="meta"]      { --tl: var(--tl-meta); }
  .dcm-tags .dcm-only[data-kind="artist"]    { --tl: var(--tl-artist); }
  .dcm-tags .dcm-only[data-kind="general"]   { --tl: var(--tl-general); }

  /* ══ FOUR PICTURES, SIDE BY SIDE ═════════════════════════════════════════
   *
   * B6.1, AND IT WAS ASKED FOR THREE TIMES. *"Also multi-selecting up to 4
   * images would be nice so they can line up next to eachother so I can
   * compare and see if they should be pooled or not. I asked this three times
   * but was never delivered. I assume there is a technical issue."*
   *
   * THERE WAS NO TECHNICAL ISSUE. It sat in `issues.md` under *Loose ends* as
   * a single bullet with no entry of its own, and Loose ends is the section a
   * round picks nothing from — so every round chose from the table above it
   * and walked past this one. Filed where work does not get scheduled, which
   * is a filing failure and not a hard problem.
   *
   * NOT THE SAME SURFACE AS `#dup-cmp`, and the difference is what drives it.
   * That one is a PAIR the archive raised a mark about, and it carries the
   * verbs for resolving a collision. This is a SELECTION — pictures somebody
   * picked because they might be one set, which no hash said anything about —
   * so it carries the two verbs that act on a set and nothing else.
   *
   * ONE ROW AND NEVER TWO. Equal columns off the count, every picture
   * `contain` inside one shared height. A grid that wraps to a second row is
   * two comparisons, which is the thing being fixed.
   */
  .lineup { position: fixed; inset: 0; z-index: 71; display: none;
    flex-direction: column; background: var(--bg-blur, rgba(10, 8, 14, 0.94));
    backdrop-filter: blur(7px); }
  .lineup.open { display: flex; }
  .lineup[hidden] { display: none; }
  .line-stage { flex: 1; min-height: 0; display: grid; gap: 12px;
    grid-template-columns: repeat(var(--line-n, 2), minmax(0, 1fr));
    grid-template-rows: minmax(0, 1fr);
    padding: 12px 14px; align-content: stretch; }
  .line-pane { display: flex; flex-direction: column; gap: 7px; min-width: 0;
    min-height: 0; }
  .line-shot { display: grid; place-items: center; min-height: 0;
    grid-template-rows: minmax(0, 1fr);
    background: #06050a; border: 1px solid var(--surface-3);
    border-radius: 12px; overflow: hidden; flex: 1; }
  .line-shot img { max-width: 100%; max-height: 100%;
    object-fit: contain; display: block; }
  /* A CURSOR AND NOT A TARGET, which is the one way this ring differs from the
     pair surface's. There the ring says which picture the verbs reach; here
     both verbs reach all of them, so it marks only where the arrow keys are
     and which pane a sentence is about. */
  .line-pane.is-at .line-shot { box-shadow: 0 0 0 2px var(--accent);
    border-color: transparent; }
  .line-pool { font-family: var(--font-mono); font-size: 0.62rem;
    color: var(--text-2); }
  /* FOUR ANSWERS, AND THE SAME FOUR WHATEVER THE TWIN IS. Which of them the
     archive has to carry out changes with the twin's status; which one you
     press does not, so they are one column of four and never eight. */
  .dcm-paths { display: flex; flex-direction: column; gap: 6px; }
  .dcm-paths .btn-op { width: 100%; justify-content: flex-start; text-align: left; }
  .dcm-paths .dcm-note { font-size: 0.66rem; color: var(--text-3);
    padding: 0 2px 2px; }

  .dcm { position: fixed; inset: 0; z-index: 70; display: none;
    flex-direction: column; background: var(--bg-blur, rgba(10, 8, 14, 0.9));
    backdrop-filter: blur(7px); }
  .dcm.open { display: flex; }
  .dcm[hidden] { display: none; }
  .dcm-head { flex: none; display: flex; align-items: center; gap: 10px;
    padding: 10px 14px; border-bottom: 1px solid var(--glass-line);
    font-size: 0.72rem; color: var(--text-3); }
  .dcm-say { color: var(--text-2); }
  /* THE LINE-UP REPORTS THROUGH THIS, because `picBar` — where the registry
     verbs normally write — is at z-index 46 and underneath that overlay. A
     refusal written somewhere nobody can see is a refusal that reads as the
     button having done nothing. Same red as the toast and the swap note. */
  .dcm-say.is-bad { color: #ffb3b3; }
  .dcm-walk { margin-left: auto; font-family: var(--font-mono); font-size: 0.62rem;
    color: var(--text-3); }
  .dcm-x { flex: none; background: transparent; border: 0; border-radius: 6px;
    color: var(--text-3); cursor: pointer; padding: 3px 7px; font-size: 0.9rem; }
  .dcm-x:hover { color: var(--text); background: var(--surface-2); }
  /* The two-pane stage was here. It assumed the only reason to open this was
     a comparison, which stopped being true when Space over a queued picture
     started opening it — see `.dcm-body` above. The flip it existed for is
     still here and still the point: the twin's picture takes the middle. */
  .dcm-body { flex: 1; min-height: 0; padding: 10px 14px; }
  #dcm-shot.is-flip { border-color: var(--accent); }
  /* WHAT THE TWO DO NOT AGREE ABOUT, which is the whole of why a resize and a
     variant look identical at 34 pixels. Shared tags are counted and not
     listed: thirty words both carry say nothing about which this is. */
  /* ── WHAT THEY DO NOT AGREE ABOUT, IN TWO COLUMNS ────────────────────
     One wrapped line held both lists and the shared count, and at thirty
     words the boundary between "only left" and "only right" was a single
     bold word somewhere in the middle of a paragraph — so the block that
     exists to show a DIFFERENCE was read as one pile.
     A rule between them and a heading over each. The headings never swap:
     left is the picture this surface was opened on, whatever the flip is
     currently showing, because a column that renames itself under a held key
     is worse than a column that needs a glance. */
  .dcm-tags { flex: none; display: grid; grid-template-columns: 1fr 1px 1fr;
    gap: 0 14px; padding: 2px 14px 10px; font-size: 0.66rem; color: var(--text-3);
    border-top: 1px solid var(--glass-line); margin-top: 6px; }
  .dcm-tags:empty { display: none; }
  .dcm-tags .dcm-rule { background: var(--glass-line); }
  .dcm-tags .dcm-col { min-width: 0; padding-top: 8px; }
  .dcm-tags .dcm-col h4 { margin: 0 0 5px; font: inherit; font-weight: 600;
    display: flex; align-items: center; gap: 6px; color: var(--text-2); }
  .dcm-tags .dcm-col h4 i { width: 7px; height: 7px; border-radius: 2px;
    flex: none; font-style: normal; }
  .dcm-tags .dcm-col.is-a h4 i { background: var(--tl-not); }
  .dcm-tags .dcm-col.is-b h4 i { background: var(--tl-meta); }
  .dcm-tags .dcm-col h4 span { font-weight: 400; color: var(--text-3);
    font-family: var(--font-mono); font-size: 0.62rem; }
  .dcm-tags .dcm-words { display: flex; flex-wrap: wrap; gap: 4px; }
  .dcm-tags .dcm-none { color: var(--text-3); opacity: 0.7; }
  .dcm-tags .dcm-foot { grid-column: 1 / -1; padding-top: 8px;
    font-family: var(--font-mono); font-size: 0.62rem; }
  .dcm-tags .dcm-only { padding: 1px 6px; border-radius: 5px;
    font-family: var(--font-mono); font-size: 0.62rem;
    background: var(--surface-2); border: 1px solid var(--glass-line); }
  .dcm-tags .dcm-only.is-a { color: var(--tl-not); }
  .dcm-tags .dcm-only.is-b { color: var(--tl-meta); }
  @media (max-width: 760px) {
    .dcm-tags { grid-template-columns: 1fr; }
    .dcm-tags .dcm-rule { display: none; }
  }
  .dcm-paths .btn-op[disabled], .dcm-verdict .btn-op[disabled] { opacity: 0.5;
    cursor: default; }
  /* ── THE WAYS OUT OF A PANE, DRAWN AS PART OF THE LINE THEY SIT IN ──────
     There was one of these — `full file` — and it was browser-default blue
     with a browser-default underline, which was survivable as a single word in
     a corner. There are three per pane now, two panes, and in the line-up up
     to four, so unstyled they are a row of hyperlink blue under every picture
     and the loudest thing on a surface whose subject is the pictures.
     A DOTTED UNDERLINE RATHER THAN NONE, because they are the only pressable
     text on the surface and everything else pressable is a button. */
  .dcm-facts a, .dcm-dim a, .dcm-twin-b a { color: var(--text-2);
    text-decoration: none; border-bottom: 1px dotted var(--surface-3); }
  .dcm-facts a:hover, .dcm-dim a:hover, .dcm-twin-b a:hover {
    color: var(--accent); border-bottom-color: currentColor; }
  .dcm-hint { font-size: 0.62rem; color: var(--text-3);
    font-family: var(--font-mono); }
  @media (max-width: 900px) {
    .dcm-body { display: block; }
    .dcm-side { width: auto; margin-top: 14px; }
  }

  /* IN A QUEUE ROW, LOUDLY: the words this picture arrived with that mean
     nothing here yet. Same family as `.snip-dup` above — a reviewer reads one
     shape, not three — but in `--tl-not` rather than the quiet line, because
     these are the one thing in the queue that REFUSES a publish rather than
     warning about it. A tint the eye can skip would be a tint that lets a
     batch sit held with nobody knowing why.
     `--tl-not` is the archive's red for removal, which is the right register:
     this is not a category, it is an alarm. */
  .snip-fix { display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
    margin: 0 0 7px; padding: 6px 8px; border-radius: 8px;
    background: color-mix(in srgb, var(--tl-not) 9%, var(--surface-2));
    border: 2px solid color-mix(in srgb, var(--tl-not) 55%, transparent);
    font-size: 0.7rem; color: var(--text-2); }
  .snip-fix .fix-lead { flex: none; font-weight: 600; color: var(--tl-not);
    letter-spacing: 0.02em; }
  /* One word, three answers. The group is the base; the buttons inside it are
     members, so the face is said once here. */
  .fixword { display: inline-flex; align-items: stretch; flex: none;
    border-radius: var(--radius-sm); overflow: hidden;
    border: 1px solid color-mix(in srgb, var(--tl-not) 45%, transparent);
    background: var(--surface-3); }
  .fixword > button { font: inherit; font-size: 0.68rem; cursor: pointer;
    border: 0; background: none; color: var(--text-2); padding: 2px 7px; }
  .fixword > button:hover { color: var(--text);
    background: color-mix(in srgb, var(--tl-not) 18%, transparent); }
  /* WHICH OF THE SOURCE'S CATEGORIES, as a leading segment and not a title:
     `copyright:kaneko_lumi` and `character:kaneko_lumi` are different words
     and the answer to one is not the answer to the other, so a reviewer has
     to be able to see which one they are about to map without hovering. */
  .fixword .fw-ns { flex: none; padding: 2px 6px; align-self: center;
    font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-3);
    background: color-mix(in srgb, var(--tl-not) 10%, transparent);
    border-right: 1px solid color-mix(in srgb, var(--tl-not) 30%, transparent); }
  /* The word itself, which is the primary press: it opens the vocabulary. */
  .fixword .fw-name { font-family: var(--font-mono); color: var(--text); }
  /* MAPPED UNDER ANOTHER CATEGORY — see unresolvedMarks. Not red: it is a
     hint about where to look, not a second alarm, and the chip it sits on is
     already inside one. */
  .fixword.is-elsewhere { border-style: solid; }
  .fixword .fw-else { flex: none; padding: 2px 7px; align-self: center;
    font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-2);
    background: var(--surface-2);
    border-left: 1px solid color-mix(in srgb, var(--tl-not) 30%, transparent); }
  .fixword .fw-x,
  .fixword .fw-nil { flex: none;
    border-left: 1px solid color-mix(in srgb, var(--tl-not) 30%, transparent); }
  /* ALREADY MAPPED, AND THE MAPPING IS THE PROBLEM. Offering the picker here
     would earn a 409 about what the word already means, so the name is not a
     button at all and the reason is on the chip instead. */
  .fixword.is-refused { border-style: dashed; }
  .fixword.is-refused .fw-name { color: var(--text-3); cursor: default; }
  .fixword .fw-why { flex: none; padding: 2px 7px; color: var(--tl-not);
    font-size: 0.64rem; align-self: center; }

  /* Drag a file anywhere and the window comes to you. Having to find a button
     before you can start is the small rudeness this removes. */
  .up-hint { position: fixed; inset: 0; z-index: 88; pointer-events: none;
    border: 2px dashed transparent; transition: border-color 0.15s ease, background 0.15s ease; }
  .up-hint.on { border-color: color-mix(in srgb, var(--accent) 55%, transparent);
    background: rgba(255, 77, 148, 0.05); }

  /* Same language as the sheet's own action row, and literally the same
     classes: .sheet-ico is a ghost square and .sheet-actions is the flex line
     they sit on.

     On every row, expanded or not. These were behind the expand gesture at
     first, on the reasoning that a strip of buttons under a thousand
     thumbnails is noise — but the row somebody most wants to grab and send on
     is a well-known one they already recognise from its title, and making them
     open it to find the download is a step that buys nothing. Three small
     ghost squares are quiet enough to sit under the tags permanently.

     `.snip .snip-acts` and not `.snip-acts`: the element carries BOTH classes,
     `.sheet-actions` is also `display: flex`, and at equal specificity the
     winner is whichever rule comes later in the stylesheet — which this one
     does not. Out-specify it and source order stops being load-bearing. */
  .snip .snip-acts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
  /* THE SIX BUTTONS IN THIS ROW ARE `.btn-ent` and state nothing of their
     own: this row was the fourth drawing of the same square (40×32, 14px
     icon) and it is the base's 30×30 now like the other three. */
  .snip-acts .btn-ent { flex: none; }
  /* The swap button keeps its wrapper, so it stays ONE item in the row with
     its own progress note beside it rather than two things the flex line can
     wrap apart. `display: contents` would have been the other way and is
     worse here: the note is this button's and nobody else's. */
  .snip-acts .snip-swap { margin: 0; flex: none; gap: 0; }
  /* The note's spacing belongs to the note. With it on the wrapper's `gap`,
     an EMPTY note still bought 8px, which read as a stray hole between the
     replace button and the one after it in a row where every other gap is 6. */
  .snip-acts .snip-swap-note:not(:empty) { margin-left: 8px; }
  /* The only button in the archive that destroys something. Coloured like it,
     and `margin-left: auto` puts it at the far end — which only works because
     it is also LAST in the markup. Placed before the re-transcribe button, the
     auto margin pushed that one out with it and the destructive one ended up
     in the middle of the group. */
  /* The irreversible one, pushed to the far end so a hand going for `unlist`
     cannot find it by being a few pixels out. Keyed on the ACT and not on
     `ent-warn`, which four buttons in this row now wear. */
  .snip-acts [data-purge] { margin-left: auto; }

  /* ── the upload bench ──────────────────────────────────────────────────
     ROWS, NOT TILES. A grid of thumbnails is the right drawing for choosing
     between pictures and the wrong one for WORKING on them: a tile has room
     for a picture and a number, and everything else this bench has to say —
     what the file is, how big, which tags are its own, what the recorder is
     doing with the link — has nowhere to go.

     A row has a long side. It is the same shape the Review panel uses on the
     same objects, so the two surfaces read as one idea seen from the two
     ends: there, you decide about things that arrived; here, you describe
     things on their way in. */
  .im-head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
    margin: 0 0 14px; }
  /* Last on the row and taking what is left. `margin-right: auto` is what
     `.im-say` carries for the foot it used to live in; here it would push
     nothing, and the answer belongs against the far edge. */
  .im-head .im-say { margin-right: 0; margin-left: auto; text-align: right; }
  /* The mint button, the height of the field beside it rather than a
     toolbar's 38. */
  .im-poolnew { width: 30px; height: 30px; border-radius: 8px; }
  .im-title { margin: 0; font-size: 1.1rem; line-height: 1.2; }
  /* One line, its own row, below the three things it is about. Allowed to
     wrap on a narrow screen — `nowrap` here would push a scrollbar across
     the whole panel to save a line break nobody minds. */
  .im-cap { font-family: var(--font-mono); font-size: 0.64rem;
    color: var(--text-3); margin: 2px 0 0; flex: 1 1 100%; }

  /* THE SEQUENCE SWITCH. Borrowed shape, archive materials: the original was
     a soft-UI pill in #ecf0f3 with light-source shadows, which is a look this
     page does not have anywhere else. What is kept is the idea — a wide
     track with the word riding inside the knob, so the control says what it
     is without a label beside it. On, the word takes the accent. */
  .toggle { position: relative; height: 30px; display: flex; align-items: center;
    gap: 7px; box-sizing: border-box; }
  .toggle input[type="checkbox"] { position: absolute; left: 0; top: 0; z-index: 2;
    width: 92px; height: 100%; margin: 0; cursor: pointer; appearance: none; }
  .toggle input[type="checkbox"]:focus-visible { outline: 2px solid var(--accent);
    outline-offset: 2px; border-radius: 15px; }
  .toggle label { position: relative; display: flex; height: 30px; align-items: center;
    box-sizing: border-box; }
  .toggle label::before { content: ''; display: inline-block; box-sizing: border-box;
    height: 30px; width: 92px; border-radius: 15px;
    background: var(--glass-sunk); border: 1px solid var(--border);
    transition: border-color 0.2s ease, background 0.2s ease; }
  /* The knob, carrying the word. `transition` on transform only — a knob that
     also fades reads as two things happening. */
  .toggle .toggle-word { position: absolute; left: 2px; top: 2px;
    height: 26px; width: 46px; border-radius: 13px;
    display: grid; place-items: center;
    font-family: var(--font-mono); font-size: 0.6rem; font-weight: 700;
    letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-3); background: var(--surface-2);
    box-shadow: 0 1px 3px rgba(0,0,0,0.3);
    transition: transform 0.28s cubic-bezier(0.85, 0.05, 0.18, 1.1),
                color 0.2s ease, background 0.2s ease; }
  .toggle input[type="checkbox"]:checked ~ label::before {
    background: var(--accent-soft); border-color: transparent; }
  .toggle input[type="checkbox"]:checked ~ label .toggle-word {
    transform: translate3d(42px, 0, 0); color: var(--accent);
    background: var(--panel); }
  @media (prefers-reduced-motion: reduce) {
    .toggle .toggle-word { transition: color 0.2s ease, background 0.2s ease; }
  }

  /* ---- the gallery's Pending switch, in the month switch's materials ------
     Same family as ONE/ALL in the calendar header: a sunken track and a knob
     that sits on top of it, and the KNOB takes the accent when it is on. The
     base switch tints the word and leaves the knob alone, which reads as a
     highlighted label rather than as a thing that has been turned on.
     The word stays inside the knob, because `Pending` is the whole of what
     this switch is about and there is no second word to slide in behind it. */
  .gal-pend label::before { background: var(--surface); border-color: transparent;
    box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.42),
                inset -2px -2px 4px rgba(255, 255, 255, 0.045); }
  .gal-pend .toggle-word { background: var(--surface-2);
    box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.38),
                -1px -1px 3px rgba(255, 255, 255, 0.05); }
  /* The track does NOT change with the knob. Two surfaces moving at once is
     the control shouting; the knob travelling and filling is enough. */
  .gal-pend input[type="checkbox"]:checked ~ label::before { background: var(--surface); }
  .gal-pend input[type="checkbox"]:checked ~ label .toggle-word {
    background: var(--accent); color: var(--on-accent); }

  .im-poolid { width: 108px; text-align: center; letter-spacing: 0.06em;
    padding: 6px 10px; border-radius: 8px; background: var(--glass-sunk);
    border: 1px solid var(--glass-line); color: var(--text);
    font-family: var(--font-mono); font-size: 0.74rem; text-transform: uppercase; }
  .im-poolid:focus { outline: none; border-color: var(--accent); }

  .im-bar { display: flex; gap: 10px; align-items: flex-start; margin: 0 0 8px;
    flex-wrap: wrap; }
  /* The pills and the way to add one, in one box — the same shape the
     per-item form's `.up-tagbox` has, for the same reason: they are one
     thing, and a button floating beside a loose row of chips reads as two. */
  .im-tagbox { flex: 1 1 100%; display: flex; flex-wrap: wrap; gap: 5px;
    align-items: center; box-sizing: border-box; min-height: 38px;
    padding: 5px 7px; border-radius: 9px; background: var(--glass-sunk);
    border: 1px solid var(--glass-line); }
  .im-tagbox:has(.tl-add:focus-visible) { border-color: var(--accent); }
  /* `.im-ac` and `.im-rtag` were the bench's own autocomplete — a floating
     list of existing names and a per-row box to type into. Both are gone:
     `tagPopover` is the picker now, for the batch and for a single row, and
     it brings its own box. */

  .im-tags { display: flex; flex-wrap: wrap; gap: 5px; margin: 0;
    align-items: center; }
  .im-tags:empty::before { content: "Nothing yet — everything below goes up bare.";
    font-size: 0.72rem; color: var(--text-3); }

  .im-rows { display: flex; flex-direction: column; gap: 8px; }

  /* The way in, which is two ways, and the verbs that act on what it makes.
     FIRST in the list rather than last: under forty rows, Upload is below
     the fold. Dashed, like everything else in this archive that is an
     outline where a thing is not yet. */
  .im-intake { display: flex; gap: 10px; flex-wrap: wrap; align-items: stretch;
    margin: 0 0 10px; }
  .im-drop { flex: 1 1 300px; min-height: 58px; display: flex; align-items: center;
    justify-content: center; gap: 9px; border-radius: 10px; padding: 10px 14px;
    font-size: 0.76rem; }
  .im-drop .ico-plus { width: 17px; height: 17px; flex: none; }
  .im-drop b { color: var(--text-2); }
  .im-rows.over .im-drop { border-color: var(--accent); color: var(--accent); }
  .im-linkrow { flex: 1 1 280px; display: flex; gap: 6px; align-items: stretch; }
  /* THE SAME HEIGHT AS THE BOX BESIDE IT, which is what 20px of vertical
     padding buys — the two are one offer and a short field next to a tall
     dashed box reads as two unrelated controls.
     NO BORDER ON FOCUS. The field is already inside a dashed outline and a
     second ring around it inside the first was the thing that looked wrong;
     the background lifting is enough to say where the caret is. */
  .im-linkrow input { flex: 1; min-width: 0; padding: 20px 13px; border-radius: 10px;
    background: var(--glass-sunk); border: 1px solid transparent;
    color: var(--text); font: inherit; font-size: 0.78rem;
    transition: background 0.12s ease; }
  .im-linkrow input::placeholder { color: var(--text-3); }
  .im-linkrow input:focus { outline: none; background: var(--surface-2); }
  /* `rv-word` is a fixed 38px box for a toolbar; the field beside it is 20px
     of padding tall so it matches the dashed drop box, and a 38px button
     floating in the middle of a 58px row is the misalignment. Stretched, the
     pair reads as one control again. */
  .im-linkrow .rv-word { height: auto; align-self: stretch; }
  /* ITS OWN ROW NOW, above the tag box rather than beside the drop target.
     Upload takes the width: it is the one you press, and a wide target for
     the act beside a narrow one for the undo is the difference said in
     geometry rather than only in colour. */
  .im-go { display: flex; gap: 8px; align-items: stretch; margin: 0 0 8px; }
  /* LONGER, NOT FULL-BLEED. It was a 110px chip; at `1 1 auto` with nothing
     to stop it, an accent slab ran the width of the page and read as a
     banner rather than a button. 420 is wide enough to be the thing you aim
     at and still be a button. */
  .im-go .im-up { flex: 1 1 auto; max-width: 420px; justify-content: center; }
  .im-go #im-clear { flex: 0 0 auto; }

  /* ── WHAT IS LEFT OF `.im-up`, WHICH IS THE ICON AND NOTHING ELSE ─────
     It carried its own size, border, padding, hover scale, press scale, a
     white inset glow and a reduced-motion override for the two transforms —
     a complete second button, on top of `btn-primary`. All of it is
     `rv-glass rv-word rv-go`'s now, which lifts and presses like every other
     pane in the archive and already honours reduced motion.
     THE TWO LINES THAT STAY are the ones about the GLYPH beside the word: no
     other `rv-word` has one, so laying it out is this class's own business
     and the only reason the class still exists. */
  .im-up { display: inline-flex; align-items: center; gap: 8px; }
  .im-up svg { width: 16px; height: 16px; flex: none; }

  /* ONE ROW. Grip, face, body, controls — and the body is a column so the
     name, the state and the row's own tags stack where there is room for
     all three. */
  /* `14px` AND NOT `10px`, which is `.snip`'s radius. A bench row and a
     queue card are the same object a minute apart — the thing you staged and
     the thing somebody reviews — and they were two corner radii. */
  .im-r { display: grid; gap: 11px; align-items: center; padding: 9px 11px;
    grid-template-columns: auto 86px minmax(0, 1fr) auto;
    border-radius: 14px; background: var(--surface);
    border: 1px solid var(--glass-line); }
  .im-r.drag { opacity: 0.35; }
  .im-r.over { border-color: var(--accent); }
  .im-r.bad { border-color: color-mix(in srgb, var(--tl-not) 45%, transparent); }
  .im-r.sent { opacity: 0.5; }
  .im-grip { display: grid; place-items: center; width: 18px; cursor: grab;
    color: var(--text-3); }
  .im-grip:active { cursor: grabbing; }
  /* Hidden rather than removed when the batch is not a sequence: the row's
     columns should not shift the moment you flip the switch. */
  .im-rows:not(.pooled) .im-grip svg { visibility: hidden; }
  .im-no { font-family: var(--font-mono); font-size: 0.62rem; font-weight: 700;
    color: var(--accent); }
  .im-rows:not(.pooled) .im-no { visibility: hidden; }
  /* 10px, the glass family's radius, inside a 14px card — the same nesting
     a queue card uses for the picture inside it. 6px read as a different
     component rather than as part of the row. */
  .im-face { width: 86px; height: 50px; border-radius: 10px; object-fit: cover;
    background: var(--surface-3); display: block; }
  .im-face.is-link { display: grid; place-items: center; color: var(--text-3); }
  /* THE PLACEHOLDER AS A BUTTON, for the one account that may press it. The
     same 86x50 box, because that is what it is about to become; what it adds
     is a cursor, a dashed edge that says "nothing here yet", and a lit one on
     hover. Nothing beside it moves, so a bench full of link rows reads the
     same whether you hold the privilege or not. */
  button.im-face.im-pf { border: 1px dashed var(--glass-line); padding: 0;
    font: inherit; cursor: pointer;
    transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease; }
  button.im-face.im-pf:hover { border-color: var(--accent); color: var(--accent);
    background: var(--surface-2); }
  button.im-face.im-pf:focus-visible { outline: 2px solid var(--accent);
    outline-offset: 2px; }
  .im-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
  .im-name { font-size: 0.82rem; color: var(--text-2); overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .im-state { font-family: var(--font-mono); font-size: 0.64rem; color: var(--text-3); }
  .im-r.bad .im-state { color: var(--tl-not); }
  .im-rtags { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
  .im-ctl { display: flex; gap: 4px; align-items: center; flex: none; }

  /* `.im-foot` is gone from the markup and `.im-say` moved into `.im-head`.
     The rule stays because the class is this archive's word for a status
     line, and the next surface that wants one should find it here. */
  .im-foot { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
  .im-say { margin-right: auto; font-size: 0.74rem; color: var(--text-3);
    min-height: 1.2em; }
  .im-say.bad { color: var(--tl-not); }

  @media (max-width: 640px) {
    .im-r { grid-template-columns: auto 64px minmax(0, 1fr); row-gap: 8px; }
    .im-ctl { grid-column: 2 / -1; }
    .im-face { width: 64px; height: 40px; }
  }

  /* ---- the audio player --------------------------------------------------
     Its own transport, because the browser's does not survive here: Chrome
     lays the control bar out against the video element's intrinsic size and an
     audio track gives it none, so it appears and then collapses. Drawing the
     waveform makes that a feature rather than a workaround — the thing you
     scrub is the shape of the sound. */
  .wave-wrap { position: absolute; inset: 0; z-index: 4; display: none;
    align-items: center; gap: 10px; padding: 0 12px; }
  .snip.big.is-sound .wave-wrap { display: flex; }
  /* The still is the fallback picture. Once the real player is up it would be
     a second, non-interactive waveform sitting behind the live one. */
  .snip.big.is-sound .snip-frame img { display: none; }
  .snip.big.is-sound .snip-play { display: none; }

  .wave-go { flex: none; width: 38px; height: 38px; border-radius: 50%; border: 0;
    display: grid; place-items: center; cursor: pointer;
    background: var(--accent); color: var(--on-accent); }
  .wave-go:hover { filter: brightness(1.1); }
  /* One canvas, not 480 elements. A div per bar is 480 nodes per open row and
     a layout pass every time the playhead moves; a canvas is one node and a
     redraw that costs nothing. */
  .wave-cv { flex: 1; min-width: 0; height: 64px; cursor: pointer; display: block; }
  /* min-width, so the clock cannot resize the canvas underneath itself. The
     text goes from "0:00" to "0:06 / 0:10" the moment a duration is known,
     which is 35px of flex shrink, a ResizeObserver tick and a visible jump of
     the whole waveform one second after you press play. tabular-nums keeps it
     still after that; this keeps it still before. */
  .wave-t { flex: none; min-width: 64px; text-align: right;
    font-family: var(--font-mono); font-size: 0.66rem;
    color: var(--text-3); font-variant-numeric: tabular-nums; }

  /* A clip with no pictures in it. The frame is the waveform's own shape, so
     the still fills it exactly instead of being letterboxed into a 16:9 hole,
     and the label says which kind of thing this is before anyone presses
     play. */
  .snip.is-sound .snip-frame { background: #07060b; }
  .snip.is-sound .snip-frame img { object-fit: contain; }
  /* A name the submitter asked for that the archive does not have. Dashed and
     grey, deliberately NOT the red of a rejected one — this is a request
     waiting on an editor, not a mistake. It never appears on a published clip,
     because by then it has either become a taglet or been dropped. */
  .tl-sugg { display: inline-flex; align-items: center; gap: 4px;
    font-family: var(--font-mono); font-size: 0.63rem; padding: 2px 7px;
    border-radius: 6px; border: 1px dashed var(--border);
    color: var(--text-3); background: none; }
  .tl-sugg::before { content: "+"; opacity: 0.7; }
  /* Only an editor can act on one, so only an editor's version looks like it
     can be. For everybody else it stays a note about the clip. */
  button.tl-sugg { cursor: pointer; }
  button.tl-sugg:hover { border-color: var(--text-3); color: var(--text-2);
    background: var(--surface-2); }

  /* Only ever rendered for staff and grant-holders — anyone else does not have
     the row at all. It is not a warning, it is a note about who can see this. */
  .gate-pill { display: inline-flex; align-items: center; gap: 4px;
    font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.04em;
    padding: 2px 6px; border-radius: 5px;
    background: color-mix(in srgb, var(--tl-not) 14%, transparent);
    color: var(--tl-not); }

  /* ── A GATE THAT DOES NOT TRAVEL ─────────────────────────────────────────
     An ordinary gate decides who may SEE a picture. This one also refuses to
     let the seeing be forwarded: no share card, no Discord embed, nothing
     served to a request that cannot say who it is. It is the stronger of the
     two, so it is drawn as the same pill wearing more rather than as a
     different colour -- full-strength gate ink, a shield, and a ring that
     glows. Reusing `--tl-not` is the point: "the same thing, but more so".
     ONE CLASS FOR THREE PLACES. The Tags-view pill, the admin chip and the
     detail-panel pill are three elements with three sizes and one meaning, so
     the marker is a modifier on each rather than a fourth component. */
  .gate-pill.shielded, .tv-gate.shielded, .ops-key.shielded {
    background: color-mix(in srgb, var(--tl-not) 22%, transparent);
    color: var(--tl-not);
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--tl-not) 60%, transparent),
                0 0 7px -1px color-mix(in srgb, var(--tl-not) 75%, transparent); }
  .gate-pill.shielded svg, .tv-gate.shielded svg, .ops-key.shielded svg {
    flex: none; margin-right: 3px; }
  /* The shield wants to sit on the text baseline rather than the box's, which
     `inline-flex` on the parent already arranges for the pills. `.ops-key` is
     not one, so it says so here instead of growing a second rule. */
  .ops-key.shielded { display: inline-flex; align-items: center; }

  /* What KIND of thing this is, in the meta colour, because that is exactly
     what --tl-meta already means everywhere else: "what it is like". A clip
     being sound-only or a gif is the same class of fact as `funny` or
     `karaoke`, so it should not invent a third colour to say so. */
  .kind-pill { display: inline-flex; align-items: center; gap: 4px;
    font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.04em;
    padding: 2px 6px; border-radius: 5px;
    background: color-mix(in srgb, var(--tl-meta) 16%, transparent);
    color: var(--tl-meta); }

  /* Converting. Deliberately NOT an error colour and not a progress bar: the
     encode has no honest percentage to report from here, and a bar that fakes
     one is worse than a word that does not. */
  .snip.is-working .snip-frame { opacity: 0.45; }
  .only-you.working { background: color-mix(in srgb, var(--accent) 14%, transparent);
    color: var(--accent); }
  .only-you.sad { background: color-mix(in srgb, var(--tl-not) 14%, transparent);
    color: var(--tl-not); }
  .oy-spin { width: 8px; height: 8px; border-radius: 50%; flex: none;
    border: 1.5px solid currentColor; border-right-color: transparent;
    animation: oy-turn 0.8s linear infinite; }
  @keyframes oy-turn { to { transform: rotate(360deg); } }
  @media (prefers-reduced-motion: reduce) { .oy-spin { animation: none; opacity: .55; } }

  /* A row only its submitter can see. Dashed, dimmer, and it says so — the
     point is that it never reads as published. */
  .snip.is-pending { border: 1px dashed color-mix(in srgb, var(--tl-meta) 42%, transparent);
    background: color-mix(in srgb, var(--tl-meta) 4%, var(--panel)); }
  .snip.is-pending .snip-frame { opacity: 0.72; }
  /* And its opposite. A clip taken back out of the archive lives in the same
     list as everything else now, so the list has to be able to say so without
     being read: dashed like the waiting one because neither is settled, red
     rather than amber because one of them is a decision and the other is a
     wait. Dimmer still, so a shelf skimmed at speed reads as published. */
  .snip.is-removed { border: 1px dashed color-mix(in srgb, var(--tl-not) 45%, transparent);
    background: color-mix(in srgb, var(--tl-not) 5%, var(--panel)); }
  .snip.is-removed .snip-frame { opacity: 0.5; }
  .only-you { display: inline-flex; align-items: center; gap: 5px;
    font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.04em;
    padding: 2px 6px; border-radius: 5px;
    background: color-mix(in srgb, var(--tl-meta) 16%, transparent); color: var(--tl-meta); }

  /* A 424px window is wrong on a phone. Same component, docked to the bottom. */
  @media (max-width: 560px) {
    .up-win { left: 8px !important; right: 8px; top: auto !important; bottom: 8px;
      width: auto; max-height: calc(var(--vh) * 82); transform-origin: bottom center; }
    .up-head { cursor: default; }
    /* The docked one is a bottom sheet here, which is what this rule always
       said -- restated because it now has a right-edge transform to undo. */
    .up-win.up-side { left: 0 !important; right: 0; top: auto !important; bottom: 0;
      width: auto; height: auto; max-height: calc(var(--vh) * 86);
      border-width: 1px 0 0; border-radius: var(--radius) var(--radius) 0 0;
      transform: translateY(100%); transform-origin: bottom center;
      box-shadow: 0 -24px 60px rgba(0,0,0,.52), inset 0 1px 0 0 var(--glass-hi); }
    .up-win.up-side.open { transform: none; }
    body.up-open .toolbar { padding-right: 0; }
  }

  /* ---- review ------------------------------------------------------------
     The same rows, plus a verdict. Deliberately NOT a diff view: judging
     whether a clip is worth publishing means watching it and reading what was
     said, so the review surface has to be the thing itself. */
  /* IN THE BAR NOW, not under it. The -12px was a tuck against a plain
     toolbar; against a padded sticky block it was an overlap, and the keys
     were drawn across the pile tabs. `flex: 1` and `flex-end` push it to the
     far side of its row, which is the right place for the one thing in the
     bar nobody presses. */
  .rv-keys { display: flex; flex-wrap: wrap; gap: 12px; margin: 0;
    flex: 1 1 auto; justify-content: flex-end;
    font-size: 0.72rem; color: var(--text-3); }
  .rv-keys b { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500;
    padding: 1px 5px; border-radius: 4px; background: var(--surface-2); color: var(--text-2); }
  /* The panel takes focus so the keymap is live without a click, which would
     otherwise ring the entire section in accent. Suppressed because the focus
     indicator that matters is .rv-at on the row — the panel is a keyboard
     host, not a control, and outlining it says nothing about where you are. */
  #panel-review:focus, #panel-review:focus-visible { outline: none; }
  .rv-progress { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text); }

  /* ══ THE QUEUE'S BAR ════════════════════════════════════════════════════
     Borrowed in shape from the companion's toolbar, which solved the same
     problem: a row of controls that has to stay reachable over content it is
     not part of. The idea there is that the BAR is nothing and each CONTROL
     is a frosted pane — so what you see moving under them is the work, and
     the controls read as floating above it rather than as a strip nailed
     over the top.
     ONE DEPARTURE, and it is about legibility rather than taste: the bar
     here also carries WORDS — the pile tabs, the progress, the pace — and
     prose over a scrolling field of thumbnails is unreadable whatever is
     behind it. So the block gets its own quiet backdrop, the same one
     `.topbar` uses, and the panes sit on that. Nothing in the companion's
     version had text loose on the bar. */
  /* ── WHAT IS PINNED IS WHAT YOU PRESS ─────────────────────────────────
     The first cut pinned the whole bar, which put the pile tabs, the
     progress and the KEYMAP at the top of the screen forever — and the
     keymap is the one thing here nobody presses. So the pinned block is the
     controls and the chips, and the row above them scrolls away like
     anything else. Less nailed down, and what stays is what a hand reaches
     for.
     AND NO PANE BEHIND IT. The backdrop was there because the bar carried
     prose, and the prose is in the row that now scrolls — so the thing it
     was protecting is gone, and what is left is the companion's own
     arrangement after all: a transparent bar and controls that are each
     their own piece of glass. */
  .rv-stick { position: sticky; top: 64px; z-index: 9;
    margin: 0 0 14px; padding: 6px 0; }
  .rv-bar { margin-bottom: 0; gap: 8px; }
  /* Row two, and no spacer in it: the controls start at the left edge and
     end wherever they end. */
  .rv-acts { margin-top: 2px; }

  /* THE ONE PIECE OF PROSE THAT STAYS NEAR THE WORK, and the only thing on
     the unpinned row that is read at a glance rather than studied. It sits
     over the page's own dotted field, where mono text at 0.78rem goes thin,
     so it gets a pane the size of itself — the same glass as the controls
     and none of the furniture, because it is a readout and not a button. */
  .rv-progress { padding: 4px 9px; border-radius: 8px;
    background: color-mix(in srgb, var(--surface) 55%, transparent);
    -webkit-backdrop-filter: blur(10px) saturate(140%);
    backdrop-filter: blur(10px) saturate(140%); }
  .rv-stick .snip-tags { margin-top: 8px; }

  /* A PANE. Translucent over whatever is behind it, a hairline that is more
     highlight than border, and an inset top edge that is what actually makes
     it read as glass rather than as a grey box with the opacity turned down. */
  .rv-glass { background: color-mix(in srgb, var(--surface) 68%, transparent);
    -webkit-backdrop-filter: blur(12px) saturate(150%);
    backdrop-filter: blur(12px) saturate(150%);
    border: 1px solid color-mix(in srgb, var(--surface-3) 55%, transparent);
    border-radius: 10px; color: var(--text-2);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18),
                inset 0 1px 0 color-mix(in srgb, #fff 7%, transparent);
    transition: transform 0.18s ease, background 0.18s ease,
                box-shadow 0.18s ease, color 0.18s ease; }
  /* THE PANE LIFTS, NOT THE GLYPH. A control that only highlights its icon
     looks like an icon that lit up; one that lifts as a whole looks like a
     thing you can press, which is the entire point of giving each of these a
     box of its own. */
  button.rv-glass:hover:not(:disabled),
  .rv-glass:has(input):focus-within {
    transform: translateY(-1px); color: var(--text);
    background: color-mix(in srgb, var(--surface-2) 82%, transparent);
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.26),
                inset 0 1px 0 color-mix(in srgb, #fff 12%, transparent); }
  button.rv-glass:active:not(:disabled) { transform: translateY(0) scale(0.97); }
  button.rv-glass:focus-visible { outline: 2px solid var(--accent);
    outline-offset: 2px; }
  /* Nothing to offer and it stops offering: no lift, no tint, and the
     pointer says so. `Undo` is this for most of a sitting. */
  button.rv-glass:disabled { opacity: 0.4; cursor: not-allowed;
    box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 5%, transparent); }

  .rv-ico { width: 38px; height: 38px; display: grid; place-items: center;
    padding: 0; cursor: pointer; }
  .rv-word { height: 38px; padding: 0 13px; font: inherit; font-size: 0.82rem;
    font-weight: 600; cursor: pointer; }
  /* The destructive one is quiet until the pointer is on it — see the note
     in the markup. */
  /* ARMED. Loud at rest is wrong in a bar you look at all day — see
     `rv-warn` below, which tints on hover — but a button that is one press
     from destroying two hundred files has stopped being a thing in a bar. */
  #rv-expunge[data-arm="1"] { color: #ff8d8d; font-weight: 650;
    background: color-mix(in srgb, #ff8d8d 16%, transparent);
    border-color: color-mix(in srgb, #ff8d8d 45%, transparent); }
  #rv-expunge[data-arm="2"] { color: var(--text-2); font-variant-numeric: tabular-nums; }
  /* ── THE ONE THAT PUBLISHES, IN THE BAR'S OWN BOX ───────────────────────
     `#rv-approve-all` wore `btn-primary` while its eight neighbours wore
     `rv-glass`, and that is not a louder control — it is a different
     material. Square 4px corners against their 10, no border against their
     hairline, 12/16 padding against a fixed 38px box, 0.88rem at weight 700
     against 0.82 at 600, and none of the blur, the hairline highlight, the
     hover lift or the press. One slab in a row of panes.
     THE COLOUR IS THE WHOLE OF WHAT IT KEEPS, which is the instruction: solid
     accent, exactly as before. Everything else — the size, the radius, the
     glass, the lift, the focus ring — comes from `rv-glass rv-word` like
     every other button in that bar.
     AFTER THE GLASS RULES AND NOT BEFORE. `button.rv-glass:hover` sets a grey
     background and `var(--text)`, at the same specificity as the hover below,
     so source order is what decides whether this stays pink when the pointer
     is on it. Moving this block above them turns the publish button grey on
     hover, silently. */
  .rv-go { background: var(--accent); color: var(--on-accent);
    border-color: color-mix(in srgb, var(--accent) 70%, transparent); }
  /* THE LIFT AND THE PRESS ARE NOT REPEATED HERE, deliberately: `transform`
     and the shadow come from the glass rules and are not overridden, so this
     button rises and squashes exactly like its neighbours. Only the two
     properties that would have turned it grey are restated. */
  button.rv-go:hover:not(:disabled) { background: var(--accent);
    color: var(--on-accent); filter: brightness(1.08); }
  .rv-warn:hover:not(:disabled) { color: #ff8d8d;
    border-color: color-mix(in srgb, #ff6b6b 45%, transparent); }

  /* ONE BUTTON, TWO DRAWINGS, and the second is the one shown while the grid
     is on — because the control offers what it will DO rather than reporting
     what you are in. `aria-pressed` still carries the state. */
  .rv-ico .ic-on { display: none; }
  .rv-ico[aria-pressed="true"] .ic-on { display: block; }
  .rv-ico[aria-pressed="true"] svg:not(.ic-on) { display: none; }
  .rv-ico[aria-pressed="true"] { color: var(--accent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
    background: color-mix(in srgb, var(--accent) 16%, transparent); }

  /* THE BOX IS A PANE TOO, which is most of why it belongs in the bar at all:
     one material, one row. The input gives up its own fill and keeps the
     magnifier's room. Capped rather than greedy — a full-width search box
     over a queue reads as the panel's subject, and the subject is the pile. */
  .rv-bar .search { flex: 0 1 320px; min-width: 190px; height: 38px;
    display: flex; align-items: center; }
  .rv-bar .search input { background: transparent; border: 0;
    padding: 0 12px 0 34px; height: 100%; font-size: 0.84rem; }
  .rv-bar .search input:focus { outline: none; }
  .rv-bar .search svg { left: 11px; }
  .rv-pace { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-3); }

  /* The cursor. Reviewing a thousand of anything is a keyboard job, and a
     keyboard job needs somewhere to look. */
  .snip.rv-at { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent-soft); }
  .snip.rv-going { opacity: 0.35; transform: translateX(10px);
    transition: opacity 0.22s ease, transform 0.22s ease; pointer-events: none; }

  .rv-verdict { grid-area: verdict; display: flex; gap: 6px; align-items: flex-start;
    padding-top: 2px; }
  /* `hasv` and not `rv`: the fourth column exists because the row CARRIES a
     verdict, and that is now true in the collection too — a waiting clip in the
     Snippets panel gets the same ✓/✗ the queue gives it. `rv` still means "this
     row is in the Review panel", which is a different fact that other code
     reads (snipRepaint asks it to rebuild the strip the way it was built), and
     collapsing the two would make a tinted collection row start claiming to be
     a queue row. */
  .snip.hasv { grid-template-columns: 210px minmax(0, 0.8fr) minmax(0, 1.25fr) auto;
    grid-template-areas: "media body tr verdict"; }
  /* The same, with the verdict kept where it was. */
  .snip.hasv.no-tr { grid-template-columns: 210px minmax(0, 1fr) auto;
    grid-template-areas: "media body verdict"; }
  .snip.hasv .snip-media { grid-area: media; }
  .snip.hasv .snip-body { grid-area: body; }
  .snip.hasv .snip-tr { grid-area: tr; }
  .snip.hasv.big { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
    grid-template-areas: "media body verdict" "media tr verdict"; }

  /* A song in the review queue, drawn as a ROW and not as the card it is in
     its own panel. The queue is one thing at a time under a cursor that J and
     K move, and a grid tile in the middle of that breaks the rhythm the whole
     panel is built on. So it borrows `.snip.rv`'s grid outright — media, body,
     third column, verdict — and only fills the cells differently. Nothing here
     re-states geometry; if the clip row's columns change, this moves with it. */
  .snip.mu-rv .snip-frame { border: 0; padding: 0; cursor: pointer;
    display: block; width: 100%; height: 100%; }
  .snip.mu-rv .snip-frame img { pointer-events: none; }
  /* One row of tags is right on a 232px card where most songs have one singer.
     Here there is a whole column, and a reviewer deciding on a submission
     wants to see every tag it arrived with, not scroll for them.

     This said `max-height: none` and did nothing for as long as it existed,
     because `.mu-tags` is sized with `height` and clamped by a mask. Undoing a
     clamp means undoing the same three properties that set it, which is what
     `.mu-minetags` already does for the add window — the two are the same
     override for the same reason. */
  .snip.mu-rv .mu-tags { height: auto; overflow: visible; flex-wrap: wrap;
    -webkit-mask-image: none; mask-image: none; }
  .snip.mu-rv .mu-ch { font-size: 0.78rem; }
  /* The third column. For a clip it is the transcript, which is what you read
     to judge it; for a song it is where it came from and what the recorder
     managed to find out — the same question, different evidence. */
  .mu-rvtr { display: flex; flex-direction: column; gap: 5px;
    font-size: 0.74rem; color: var(--text-2); }
  .mu-rvline { display: flex; gap: 9px; align-items: baseline; }
  .mu-rvline > b { flex: 0 0 60px; color: var(--text-3); font-weight: 500;
    font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.04em;
    text-transform: uppercase; }
  .mu-rvline > span { min-width: 0; overflow-wrap: anywhere; }
  .mu-rvline a { color: var(--text-2); }
  .mu-rvline a:hover { color: var(--accent); }

  .rv-btn { width: 34px; height: 34px; display: grid; place-items: center; padding: 0;
    border: 1px solid var(--surface-3); border-radius: 9px; background: var(--surface);
    color: var(--text-2); cursor: pointer;
    transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease; }
  /* A CONTROL THAT CANNOT ACT MUST NOT LOOK AS IF IT CAN. The publish button
     is disabled while a word is outstanding, and left unstyled it kept its
     full contrast and its pointer — so the only feedback was a press that did
     nothing, which is the failure the disabled state exists to prevent. No
     hover either: an affordance that lights up under the cursor is a promise.
     Members, not the base, so the one button this applies to is the one that
     changes. */
  .rv-btn:disabled { opacity: 0.38; cursor: not-allowed; }
  .rv-btn:disabled:hover { background: var(--surface); color: var(--text-2);
    border-color: var(--surface-3); }
  .rv-btn:hover { background: var(--surface-2); color: var(--text); }
  .rv-yes:hover { border-color: #6fcf87; color: #6fcf87; }
  .rv-no:hover  { border-color: #ff6b6b; color: #ff6b6b; }

  /* An auto-generated title is the thing most likely to need a human, so it is
     editable where it sits rather than behind a dialog. */
  .rv-title { cursor: text; border-radius: 5px; padding: 0 3px; margin: 0 -3px; }
  .rv-title:hover { background: var(--surface); }
  .rv-title-in { width: 100%; font: inherit; font-family: var(--font-display);
    font-weight: 600; font-size: 0.95rem; color: var(--text);
    background: var(--surface); border: 1px solid var(--accent); border-radius: 6px;
    padding: 2px 6px; }
  .rv-flag { font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.04em;
    padding: 2px 6px; border-radius: 5px; background: var(--surface-2); color: var(--text-3); }
  .rv-flag.warn { color: #e0c463; }

  @media (max-width: 1000px) {
    .snip.hasv { grid-template-columns: 160px minmax(0, 1fr) auto;
      grid-template-areas: "media body verdict" "tr tr tr"; }
    /* Restated HERE rather than left to specificity. `.snip.hasv.no-tr`
       outranks `.snip.hasv` whatever the source order, and a media query adds
       none — so a wide-layout override written once would follow the card onto
       a phone and bring the desktop's 210px media cell with it. */
    .snip.hasv.no-tr { grid-template-columns: 160px minmax(0, 1fr) auto;
      grid-template-areas: "media body verdict"; }
  }

  @media (max-width: 1000px) {
    .snip { grid-template-columns: 160px minmax(0, 1fr); }
    .snip.no-tr { grid-template-columns: 160px minmax(0, 1fr); }
    .snip-media { width: 160px; }
    .snip-tr { grid-column: 1 / -1; max-height: 96px; }
    .snip.big { grid-template-columns: minmax(0, 1fr);
      grid-template-areas: "media" "body" "tr"; }
    .snip.big .snip-tr { max-height: 140px; }
  }
  .clip { display: flex; align-items: center; gap: 16px; background: var(--surface); border-radius: var(--radius); padding: 12px; }
  .clip .thumb { flex: 0 0 132px; aspect-ratio: 16/9; border-radius: 8px; }
  .clip .body { flex: 1 1 auto; min-width: 0; }
  .clip .title { font-weight: 600; }
  .clip .sub { display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap; }

  /* Stream grid */
  .vod { background: var(--surface); border-radius: var(--radius); overflow: hidden; }
  .vod .thumb { aspect-ratio: 16/9; transition: filter 0.15s ease; }
  .vod .thumb .vod-idx { position: absolute; left: 8px; bottom: 8px; z-index: 1;
    font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.06em; font-variant-numeric: tabular-nums;
    color: var(--text-2); background: rgba(20, 18, 25, 0.72); backdrop-filter: blur(4px);
    padding: 2px 6px; border-radius: 5px; }
  .vod .thumb .badges { position: absolute; top: 8px; right: 8px; z-index: 1; display: flex; gap: 6px; }
  /* Icon badges, sized and tinted like the theater's source tiles. The dark
     ground is not decoration: these sit on a thumbnail, and a mark alone
     disappears over a pale frame. The ground is the whole of it — no outline,
     because the GLYPH carries the answer and a box drawn round it says
     nothing the glyph has not already said. */
  .cbadge { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center;
    background: rgba(20, 18, 25, 0.72); backdrop-filter: blur(4px);
    border: 0; color: var(--text-3); }
  .cbadge.on { color: var(--accent); }
  .cbadge svg { width: 14px; height: 14px; display: block; }
  /* The side padding is a variable because the alt-title ridge has to cancel
     it exactly — it hangs off the body's border edge, not its content edge,
     and two places holding 14px is how that drifts. */
  .vod .body { --body-pad-x: 14px; padding: 13px var(--body-pad-x) 15px;
               position: relative; }
  /* CHECKED BY HAND. A watermark rather than a badge: the corner it sits in
     is the one the wrench and the bin arrive in on hover, and the row they
     live in already reserves their width at `opacity: 0` — so this is laid
     over space the layout had set aside, moves nothing in either state, and
     with `pointer-events: none` was never in the way of the click. The fade
     is so the two are not drawn on top of each other.
     `--text-2` and not the accent: three accent objects on one card, after
     the two lit source badges, reads as something happening rather than
     something settled. */
  .vod-ok { position: absolute; right: 5px; bottom: 3px;
    width: 26px; height: 26px; color: var(--text-2); opacity: 0.85;
    pointer-events: none; transition: opacity 0.12s ease; }
  .vod-ok svg { width: 100%; height: 100%; display: block; }
  .vod:hover .vod-ok { opacity: 0; }
  /* Two lines, then an ellipsis. min-height reserves the second line so a grid
     of mixed one- and two-line titles keeps its rows aligned instead of
     stepping. The full title stays in the title= attribute. */
  .vod .title { font-weight: 600; line-height: 1.35;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    overflow: hidden; min-height: 2.7em; }
  .vod .sub { display: flex; gap: 8px; align-items: center; margin-top: 9px; }
  /* The other platform's title, when there is one. Fixed width and always
     present so the row beside it does not shuffle from card to card — the
     unlit state is the same box holding a dash, which is what makes a lit one
     findable by scanning rather than by reading. Muted on purpose: it is a
     footnote about the title, not a second title. */
  /* A ridge down the left of the title, the height of the title block. Not a
     badge: it carries no text, takes no space in any row of controls, and
     cannot push anything around — which is what the badge in the meta row did
     to the wrench and the bin.
     The title box is a 2-line `-webkit-line-clamp`, so anything INSIDE it is
     inline content and gets clipped away on exactly the long titles most
     likely to differ between platforms. A sibling in a flex row is not inline
     content, so `align-self: stretch` matches the clamp's height for free and
     the clipping problem never arises. */
  .vod .t-row { display: flex; align-items: stretch; gap: 9px; position: relative; }
  .vod .t-row .title { flex: 1; min-width: 0; }
  /* 5px of paint in an 11px target: a five-pixel hover area is a hover area
     nobody hits, and `background-clip: content-box` lets the padding be
     grabbable without being visible.
     `background-COLOR` and not the `background` shorthand, which is the whole
     reason this needed a second pass: the shorthand resets every background
     longhand it does not name, so a `background:` declared after
     `background-clip` silently put the clip back to `border-box` and painted
     the padding too. The ridge shipped three times wider than it was written
     to be, and the suite agreed with the stylesheet because it asserted the
     declared `width` rather than the pixels. */
  /* Flush with the card's left edge, on the same vertical line as the
     thumbnail above it — NOT inset by the body's padding, which is where a
     flex child of the row necessarily sits. So it comes out of the flow:
     absolute against `.t-row`, pulled back by exactly the padding the body
     put in, `top`/`bottom` doing what `align-self: stretch` used to do.

     Out of the flow also means the title is no longer pushed along by it, so
     the title starts at the body padding like every other line in the card
     and gets the 20px back. The gap to the ridge is then the padding minus
     the paint — 9px, which is what `gap` was setting by hand.

     Padding on the RIGHT only. The 3px on the left used to be half the hover
     target; here it would push the paint 3px inboard, which is the whole
     thing being asked for undone. All 6px of it goes right instead, so the
     target is the same 11px it always was and it grows into the gap, where
     there is nothing to collide with. */
  .alt-t { position: absolute; top: 0; bottom: 0;
    left: calc(var(--body-pad-x, 14px) * -1);
    width: 5px; padding: 0 6px 0 0; box-sizing: content-box;
    /* Square, deliberately. A radius on a 5px bar sitting ON the card's
       edge leaves two slivers of card showing at the ends and reads as a
       chip that missed its mark; the whole point is that it continues the
       thumbnail's edge, and an edge does not have corners. */
    border-radius: 0;
    background-color: var(--alt-on, var(--accent));
    background-clip: content-box; cursor: help; user-select: none; }
  .alt-t:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  /* Nothing to say, and it says nothing — but it holds the line, so a lit one
     is found by scanning a column rather than by reading every card. */
  .alt-t.is-none { background-color: var(--alt-off, rgba(255, 255, 255, 0.10));
    cursor: default; }

  /* ---- Calendar ---- */
  /* The panel is a plain region — the dot field behind it is the page background. */
  #panel-index {
    --cal-track: rgba(255, 255, 255, 0.075);
    --st-held: #7bc89b;
    --st-missed: #e07070;
    --st-unknown: #b8b5bd;
  }

  .cal-layout { display: flex; gap: 30px; align-items: flex-start; }
  .calendar-main { position: relative; flex: 0 0 380px; width: 380px; }
  .cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
  .cal-title { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: 1.12rem; letter-spacing: -0.02em; }
  .cal-nav { display: flex; gap: 5px; align-items: center; }
  .cal-nav .btn-ghost { padding: 5px 7px; border-radius: 8px; }
  .cal-nav .btn-ghost svg { width: 13px; height: 13px; }
  /* The month arrows wear the Review panel's glass. SCOPED SMALLER, the way
     the upload window already scopes it: `.rv-ico` is a 38px square and this
     row lives in a fixed 380px column that also has to hold the title and
     the switch, so two of them at full size costs the title its line. 28
     keeps the footprint the ghost buttons had. */
  .cal-nav .rv-ico { width: 28px; height: 28px; border-radius: 9px; }
  .cal-nav .rv-ico svg { width: 13px; height: 13px; }
  .cal-dow { display: grid; grid-template-columns: repeat(7,1fr); gap: 4px; margin-bottom: 4px; }
  .cal-dow span { font-family: var(--font-mono); font-size: 0.5rem; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-3); text-align: center; }
  .cal-grid { display: grid; grid-template-columns: repeat(7,1fr); gap: 4px; }

  /* ── Day card: gradient ring + inset inner (tenma stream card) ── */
  /* 10px outside, 5px on the face, and the 5px between them is the ring: two
     rounded rectangles are parallel offsets of one another only when the inner
     radius is the outer MINUS the gap. At 5px/3.5px they were not a pair, and
     the corners picked up the 3.5px difference -- 1.29x the width of the
     sides, measured. As a pair the corners come out even, and on the lifted
     face it also buys a pixel back at the top.
     9 and 5 with a 4px ring is that pair exactly, with nothing to reconcile. */
  .cal-cell {
    position: relative; aspect-ratio: 1; border-radius: 9px;
    background: var(--cal-track);
    transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  .cal-cell::before {
    content: ''; position: absolute; inset: -1px; border-radius: 10px;
    background: var(--seg, none); filter: blur(4px); opacity: 0.5; z-index: 0;
    transition: filter 300ms ease, opacity 300ms ease;
  }
  .cal-cell::after {
    content: ''; position: absolute; inset: 0; border-radius: 9px;
    background: var(--seg, none); z-index: 1;
  }
  .cal-inner {
    position: absolute; border-radius: 5px;
    /* OFF CENTRE BY ONE PIXEL, on purpose: 4px of ring above the face and 6px
       below it, same face, moved up.
       The shadow below stays where it is, and it is offset downward -- the
       face is z-index 2 and the arc is z-index 1, so that shadow paints ON TOP
       of the ring, and only along the bottom. Centred, it left a bottom edge
       reading 4px on two cells in three and 5px on the rest. Given a 6px ring
       to chew on it leaves exactly 5px, on all 21 cells measured, which is the
       same 5px the sides read. Compensating for the shadow rather than moving
       it. */
    inset: calc(var(--cal-ring) - 1px) var(--cal-ring) calc(var(--cal-ring) + 1px);
    background: var(--text); z-index: 2;
    display: flex; flex-direction: column; justify-content: center; align-items: center;
    overflow: hidden;
    box-shadow: 0 1px 2px rgba(0,0,0,0.45), 0 3px 8px rgba(0,0,0,0.35);
    /* `inset` joins the list, and it is the whole of the wide-ring movement.
       The same curve the cell's own lift uses, so the ring opening and the
       card rising read as one gesture rather than two that nearly agree. */
    transition: background 200ms ease, box-shadow 300ms ease,
                inset 260ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  .cal-num {
    font-family: var(--font-display); font-size: 0.8rem; font-weight: 700; line-height: 1;
    letter-spacing: -0.03em; color: var(--surface-2); font-variant-numeric: tabular-nums;
    transition: color 200ms ease;
  }
  /* dark-on-light wants the heavier stem weight back */
  .cal-cell.streamed .cal-num { -webkit-font-smoothing: auto; }

  /* days with no stream: flat, inert */
  .cal-cell:not(.streamed) { background: transparent; pointer-events: none; }
  .cal-cell:not(.streamed) .cal-inner { inset: 0; border-radius: 9px; background: transparent; box-shadow: none; }
  .cal-cell:not(.streamed) .cal-num { color: var(--text-3); font-weight: 600; }
  .cal-cell.cal-empty { background: transparent; }

  .cal-cell.streamed { cursor: pointer; }
  /* THE WIDE RING, on the day you opened.
     `--cal-ring` is declared on `:root` and re-declared here: `.cal-inner`
     reads it through `var()`, so redefining it on the cell reaches the face
     without either of them naming the other.
     Selection only, not hover -- the ring is the answer to "which day am I
     looking at", and a 6px lurch under the pointer on the way somewhere else
     is noise. The movement lives on `.cal-inner`'s `inset`: the custom
     property itself does not animate (that would want @property), but the
     four lengths it feeds do, which is the same thing on screen.
     It stops being a matched pair at this width -- 9 - 5 = 4, not 10 -- so
     the corners take the difference while a day is open. Left alone, because
     it is a look somebody chose rather than a defect. */
  .cal-cell.selected { --cal-ring: 10px; }
  .cal-cell.streamed:hover, .cal-cell.selected { transform: translateY(-2px); }
  .cal-cell.streamed:hover::before, .cal-cell.selected::before { filter: blur(8px); opacity: 0.9; }
  .cal-cell.streamed:hover .cal-inner, .cal-cell.selected .cal-inner {
    background: #ffffff;
    box-shadow: 0 2px 4px rgba(0,0,0,0.45), 0 7px 16px rgba(0,0,0,0.4);
  }
  .cal-cell.streamed:hover .cal-num { color: var(--bg); }
  .cal-cell.selected .cal-num { color: var(--accent); }
  .cal-cell.today .cal-inner { box-shadow: inset 0 0 0 1px var(--accent), 0 1px 2px rgba(0,0,0,0.45); }

  /* status dots under the number */
  .cal-dots { display: flex; gap: 2.5px; margin-top: 3px; }
  .cal-dot { width: 4px; height: 4px; border-radius: 50%; transition: transform 300ms ease; }
  .dot--held { background: var(--st-held); }
  .dot--missed { background: var(--st-missed); }
  .dot--unknown { background: var(--st-unknown); }
  .dot--scheduled { background: transparent; box-shadow: inset 0 0 0 1.2px var(--st-held); }
  .cal-cell.streamed:hover .cal-dot:nth-child(1) { transform: scale(1.4); transition-delay: 0ms; }
  .cal-cell.streamed:hover .cal-dot:nth-child(2) { transform: scale(1.4); transition-delay: 40ms; }

  /* legend */
  .cal-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 10px; margin-top: 14px; }
  .cal-legend-item { display: flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 0.52rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-3); }
  .cal-legend-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
  .cal-legend-ring { width: 9px; height: 9px; border-radius: 2.5px; flex-shrink: 0; background: conic-gradient(from 0deg, transparent 0 200deg, var(--air) 200deg 300deg, transparent 300deg 360deg); }

  /* ── Day popover: floats over the layout, never resizes the grid ── */
  .cal-pop {
    position: absolute; z-index: 40; width: 322px;
    background: var(--panel); border-radius: 14px;
    box-shadow: 0 0 0 1px rgba(255,255,255,0.075), 0 20px 52px rgba(0,0,0,0.6);
    padding: 13px 13px 11px;
    opacity: 0; transform: scale(0.97); transform-origin: left center;
    transition: opacity 150ms ease, transform 180ms cubic-bezier(0.22,1,0.36,1);
    pointer-events: none;
  }
  .cal-pop.open { opacity: 1; transform: none; pointer-events: auto; }
  .cal-pop[data-side="left"] { transform-origin: right center; }
  .cal-pop::before {
    content: ''; position: absolute; top: var(--caret-y, 28px);
    width: 10px; height: 10px; background: var(--panel);
    transform: translateY(-50%) rotate(45deg);
  }
  .cal-pop[data-side="right"]::before { left: -4px; box-shadow: -1px 1px 0 rgba(255,255,255,0.075); }
  .cal-pop[data-side="left"]::before { right: -4px; box-shadow: 1px -1px 0 rgba(255,255,255,0.075); }

  .pop-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
  .pop-date { font-family: var(--font-display); font-weight: 700; font-size: 0.92rem; }
  .pop-sub { font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-3); }
  .pop-close { background: var(--surface-2); border: 0; color: var(--text-2); width: 24px; height: 24px; border-radius: 7px; display: grid; place-items: center; flex-shrink: 0; cursor: pointer; }
  .pop-close:hover { background: var(--surface-3); color: var(--text); }
  .pop-list { max-height: calc(var(--vh) * 56); overflow-y: auto; display: flex; flex-direction: column; gap: 5px; }

  .pop-item { background: var(--surface); border-radius: 9px; overflow: hidden; }
  .pop-item > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px; padding: 8px 10px; }
  .pop-item > summary::-webkit-details-marker { display: none; }
  .pop-item > summary:hover { background: var(--surface-2); }
  .pop-time { font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-3); flex-shrink: 0; }
  /* min-width: 0 is what makes the ellipsis fire. Without it a flex item's
     automatic minimum size is its max-content width, so a nowrap title refuses
     to shrink, never truncates, and instead overruns the 322px popover — with
     .pop-item's overflow:hidden clipping the status dots and chevron off the
     right edge instead. */
  .pop-title { font-size: 0.8rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .pop-status { display: flex; gap: 3px; margin-left: auto; flex-shrink: 0; }
  .pop-chev { flex-shrink: 0; color: var(--text-3); transition: transform 180ms ease; }
  .pop-item[open] .pop-chev { transform: rotate(90deg); }
  .pop-item[open] > summary { background: var(--surface-2); }
  .pop-body { padding: 2px 10px 10px; }
  .pop-meta { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 8px; }
  .pop-summary { font-size: 0.74rem; line-height: 1.5; color: var(--text-2); }
  .pop-notes { margin-top: 9px; display: flex; flex-direction: column; gap: 1px; }
  .pop-notes-head { font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); margin-bottom: 4px; }
  .ob-line { display: flex; align-items: center; gap: 4px; font-size: 0.73rem; }
  .ob-check { display: flex; align-items: center; gap: 7px; flex: 1 1 auto; min-width: 0; padding: 3px 4px; border-radius: 5px; cursor: pointer; }
  .ob-check:hover { background: var(--surface-2); }
  /* A button, not a checkbox input. It writes through a changeset like every
     other decision, and a native box that flips the instant you click it would
     be claiming a write that may still be queued for review. */
  .ob-box { appearance: none; border: 0; padding: 0; width: 11px; height: 11px; flex: none;
    border-radius: 3px; background: transparent; box-shadow: inset 0 0 0 1.2px var(--text-3);
    cursor: pointer; }
  .ob-box[aria-pressed="true"] { background: var(--air); box-shadow: inset 0 0 0 1.2px var(--air); }
  .ob-box:hover:not(:disabled) { box-shadow: inset 0 0 0 1.2px var(--accent); }
  .ob-box:disabled { cursor: default; opacity: 0.55; }
  /* Untagged notes are observations, not tasks, and get no box — but they keep
     the indent so the text of every row still starts on the same column. */
  .ob-nobox { width: 11px; flex: none; }
  .ob-tag { font-family: var(--font-mono); font-size: 0.68rem; color: var(--air); flex-shrink: 0; }
  /* The tag moved out of every row and into one header per group. Repeating it
     on each line was the same word four times in a column 320px wide. */
  .ob-group { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: var(--text-3); margin: 9px 0 3px; }
  .ob-group:first-child { margin-top: 0; }
  /* Two states, not the theater's five: this card has no source list to name a
     clock from, so all it can say is whether anybody ever did. */
  .ob-clk { width: 5px; height: 5px; border-radius: 50%; flex: none;
    box-shadow: inset 0 0 0 1px var(--text-3); }
  .ob-clk.on { background: var(--accent); box-shadow: none; }
  .ob-new { margin-top: 10px; }
  .ob-new textarea { width: 100%; box-sizing: border-box; background: transparent;
    border: 1px dashed rgba(255,255,255,0.14); border-radius: 8px; color: var(--text);
    padding: 6px 9px; font-family: inherit; font-size: 0.73rem; }
  .ob-new textarea::placeholder { color: var(--text-3); }
  .ob-new textarea:focus { outline: none; border-style: solid; border-color: var(--accent);
    background: var(--surface-2); }
  .ob-new textarea:disabled { opacity: 0.5; }
  .ob-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
  /* Dimmed, never struck through. A line through a sentence is hard to read and
     says "wrong" when what it means is "handled". The filled box says that. */
  .ob-text.done { color: var(--text-3); }
  .ob-text i { opacity: 0.5; }
  .ob-ts { flex-shrink: 0; background: transparent; border: 0; padding: 2px 4px; border-radius: 4px; cursor: pointer;
           font-family: var(--font-mono); font-size: 0.64rem; color: var(--text-3); font-variant-numeric: tabular-nums; }
  .ob-ts:hover { background: var(--surface-2); color: var(--air); }
  .pop-empty { font-size: 0.73rem; color: var(--text-3); font-style: italic; }

  /* ── Month's streams, to the right of the grid ── */
  .cal-streams { flex: 1 1 auto; min-width: 0; }
  .cal-streams-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 12px; min-height: 30px; }
  .cal-streams-title { font-family: var(--font-display); font-weight: 700; font-size: 1.12rem; letter-spacing: -0.02em; }
  /* Capped rather than 1fr. With a 1fr max the track width is available/n, so
     opening the notes rail resized every card at once; capping it means the
     COUNT changes and the cards do not. Leftover width collects at the right,
     where it reads as margin.

     245 is deliberately NOT the value that packs the peek state exactly. The
     rail reclaims ~350px against a ~259px column pitch — 1.35 columns — so
     whether you lose one column or two depends on which side of a threshold the
     fraction lands, and layout widths are fractional at a non-integer
     devicePixelRatio. Packing it exactly puts the count ON that threshold and it
     tips to two. This sits ~50px clear of the boundary: one column, reliably,
     from about 1865px of viewport upward. */
  /* ── The view picker ────────────────────────────────────────────────────
     Always a button, never a span that becomes one on hover. The hover state
     is the emphasis, not the affordance: a control that only exists once the
     pointer is on it cannot be found by anyone who does not already know it is
     there, and does not exist at all on a touch screen. The chevron sits at
     0.45 opacity at rest and comes up to full on hover and focus, which reads
     as "this does something" without shouting. */
  .view-pick { position: relative; }
  .view-pick-btn { display: inline-flex; align-items: center; gap: 6px;
    padding: 2px 6px; margin-left: -6px; border: 0; background: transparent;
    color: inherit; cursor: pointer; border-radius: 8px;
    transition: background 0.15s ease, color 0.15s ease; }
  .view-pick-btn:hover { background: var(--surface); }
  .view-chev { flex: none; opacity: 0.45; transition: opacity 0.15s ease, transform 0.18s ease; }
  .view-pick-btn:hover .view-chev, .view-pick-btn:focus-visible .view-chev { opacity: 1; }
  .view-pick-btn[aria-expanded="true"] .view-chev { opacity: 1; transform: rotate(180deg); }
  .view-menu { position: absolute; top: calc(100% + 6px); left: -6px; z-index: 30;
    min-width: 150px; padding: 5px; background: var(--surface-2);
    border-radius: var(--radius-sm); box-shadow: 0 12px 32px rgba(0,0,0,0.45),
    inset 0 0 0 1px rgba(255,255,255,0.06); display: flex; flex-direction: column; gap: 2px; }
  .view-menu[hidden] { display: none; }
  .view-menu button { text-align: left; border: 0; background: transparent; color: var(--text-2);
    font: inherit; font-size: 0.82rem; padding: 7px 10px; border-radius: 7px; cursor: pointer; }
  .view-menu button:hover { background: var(--surface-3); color: var(--text); }
  .view-menu button[aria-current="true"] { color: var(--accent); font-weight: 600; }

  /* ── Notes view ─────────────────────────────────────────────────────────
     One block per stream, and a stream with nothing on it still gets its line:
     the whole point is seeing where the gaps are. The header is the lighter
     band; the notes hang off it, indented, with a rule down the left so the
     indent reads as containment rather than as a margin. */
  /* Capped. These rows were drawn for a 320px rail and the column they are in
     is now 1400+ wide — unbounded, the text sits at the far left and the
     timestamp at the far right with a field of nothing between them, and the
     eye has to travel the whole way to pair them up. A document width reads. */
  .cal-alt { max-width: 1080px; }
  .cal-alt[hidden] { display: none; }
  .nv-stream { margin-bottom: 8px; }
  .nv-head { display: flex; align-items: center; gap: 10px; width: 100%;
    padding: 8px 12px; border: 0; border-radius: var(--radius-sm);
    background: var(--surface-2); color: var(--text-2); font: inherit;
    font-size: 0.8rem; text-align: left; cursor: pointer;
    transition: background 0.15s ease; }
  .nv-head:hover { background: var(--surface-3); }
  .nv-chev { flex: none; transition: transform 0.18s ease; opacity: 0.5; }
  .nv-stream.is-shut .nv-chev { transform: rotate(-90deg); }
  .nv-idx { font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-3);
    background: var(--surface); padding: 2px 6px; border-radius: 5px; flex: none; }
  .nv-when { font-family: var(--font-mono); font-size: 0.64rem; color: var(--text-3); flex: none; }
  .nv-title { font-weight: 600; color: var(--text); overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
  .nv-count { flex: none; font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-3); }
  .nv-count.none { opacity: 0.5; }
  .nv-body { padding: 4px 0 6px 16px; margin-left: 12px;
    border-left: 1px solid rgba(255,255,255,0.07); }
  .nv-stream.is-shut .nv-body { display: none; }
  .nv-empty { padding: 6px 2px; font-size: 0.74rem; color: var(--text-3); font-style: italic; }

  /* ── Tags view ──────────────────────────────────────────────────────────── */
  /* Sticky, because the list is now the whole vocabulary and the filter you
     set forty rows ago has to still be visible when you get there. */
  .tv-bar { position: sticky; top: 0; z-index: 3; background: var(--bg);
    padding: 2px 0 8px; margin-bottom: 2px; }
  /* The press at the end of the list. `.tv-more` already centres and sizes
     the sentinel it stands in for; this adds only what makes it pressable. */
  button.tv-load { display: block; width: 100%; border: 0; font: inherit;
    background: var(--surface-2); color: var(--text-2); border-radius: 8px;
    padding: 7px 10px; cursor: pointer; transition: 0.13s; }
  button.tv-load:hover:not(:disabled) { background: var(--surface-3); color: var(--text); }
  button.tv-load:disabled { opacity: 0.6; cursor: default; }
  .tv-search { width: 100%; border: 0; border-radius: var(--radius-sm);
    background: var(--surface); color: var(--text); font: inherit; font-size: 0.8rem;
    padding: 8px 11px; outline: 1px solid transparent; }
  .tv-search:focus { outline-color: var(--accent); }
  .tv-search::placeholder { color: var(--text-3); }
  .tv-bar .pop2-kinds { padding: 7px 0 0; }
  /* The swatch carries the colour; an unpicked chip should not also carry it,
     or every filter looks half-on. */
  .tv-bar .pop2-kinds button { font-size: 0.58rem; }
  .tv-bar .pop2-kinds button[aria-pressed="true"] i { box-shadow: 0 0 0 1.5px var(--on-accent); }
  .tv-tally { font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--text-3); padding: 7px 2px 0; }
  .tv-more { padding: 10px 2px; text-align: center; color: var(--text-3);
    font-size: 0.72rem; }
  /* A container, not a button.
     The name and the kind are each their own control now — clicking the name
     renames and clicking the kind re-files — and a <button> can hold neither:
     nested interactive elements are invalid and the inner ones stop working,
     and an <input> inside a button cannot be typed in, which is what a rename
     swaps into the name. So the row is a div, the grip and the row's own
     background toggle it, and everything else in it acts for itself. */
  .tv-row { display: flex; align-items: center; gap: 12px; width: 100%;
    padding: 7px 12px; margin-bottom: 5px; border-radius: var(--radius-sm);
    background: var(--surface); color: var(--text-2); font-size: 0.8rem;
    text-align: left; cursor: pointer; transition: background 0.15s ease; }
  .tv-row:hover { background: var(--surface-2); }
  /* The chevron and the art, together, as one focusable thing — so the row
     can still be opened from the keyboard now that the head is not a button. */
  .tv-grip { display: flex; align-items: center; gap: 12px; flex: none; border: 0;
    padding: 0; background: none; color: inherit; font: inherit; cursor: pointer; }
  .tv-grip:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px;
    border-radius: 5px; }
  .tv-art { flex: none; width: 30px; height: 40px; border-radius: 5px; overflow: hidden;
    position: relative; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06);
    background: linear-gradient(150deg, hsl(var(--hue) 34% 24%), hsl(calc(var(--hue) + 40) 28% 15%)); }
  .tv-art img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
  .tv-art b { position: absolute; inset: 0; display: grid; place-items: center;
    font-family: var(--font-display); font-size: 0.8rem; color: rgba(255,255,255,0.3); }
  .tv-name { font-weight: 600; color: var(--text); flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    padding: 2px 5px; margin: -2px -5px; border-radius: 5px; }
  /* Only when there is somebody to act. A dotted underline on hover rather
     than a permanent box: the name is the loudest thing in the row, and a
     frame around it all the time reads as an empty field. */
  .tv-name.live { cursor: text; text-decoration: underline dotted transparent;
    text-underline-offset: 3px; transition: background 0.12s ease; }
  .tv-name.live:hover, .tv-name.live:focus-visible { background: var(--glass-sunk);
    text-decoration-color: var(--text-3); outline: 0; }
  .tv-kind { flex: none; display: inline-flex; align-items: center; gap: 5px;
    border: 0; background: none; padding: 2px 5px; border-radius: 5px;
    font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--text-3); }
  .tv-kind.live { cursor: pointer; transition: background 0.12s ease, color 0.12s ease; }
  .tv-kind.live:hover, .tv-kind.live:focus-visible { background: var(--glass-sunk);
    color: var(--text-2); outline: 0; }
  .tv-kind i { width: 7px; height: 7px; border-radius: 2px; transform: rotate(45deg); }
  /* `unlabelled` has no colour to show — --seg-unknown is transparent on
     purpose — so it gets an outline instead of an invisible swatch. Legacy
     now: nothing new is minted without a kind. */
  .tv-kind.unfiled i { box-shadow: inset 0 0 0 1px var(--text-3); }
  .tv-parent { flex: none; font-size: 0.68rem; color: var(--text-3); }
  /* A button that reads as a label until you go near it — the same bargain
     `.tv-kind.live` makes, because this sits in a row whose whole surface is
     a toggle and a second thing that looks pressable would be noise. */
  button.tv-parent { border: 0; border-radius: 5px; padding: 2px 5px;
    background: none; font: inherit; font-size: 0.68rem; color: var(--text-3);
    cursor: pointer; transition: 0.13s; }
  button.tv-parent:hover { background: var(--surface-2); color: var(--text-2); }
  .tp-res { display: flex; flex-direction: column; max-height: 212px;
    overflow: auto; margin: 6px 0 2px; }
  .tp-res button { display: flex; align-items: baseline; gap: 7px; padding: 5px 7px;
    border: 0; border-radius: 6px; background: none; color: var(--text-2);
    font: inherit; font-size: 0.76rem; text-align: left; cursor: pointer; }
  .tp-res button:hover { background: var(--surface-2); color: var(--text); }
  .tp-res button[disabled] { opacity: 0.45; cursor: not-allowed; }
  .tp-res .k { font-size: 0.62rem; color: var(--text-3); }
  .tp-res .u { margin-left: auto; font-size: 0.62rem; color: var(--text-3);
    font-variant-numeric: tabular-nums; }
  .tp-note { font-size: 0.66rem; color: var(--text-3); padding: 4px 7px; }
  /* Stacked, and only the lines that say something.
     Side by side this wrapped on a wide screen the moment a tag had two kinds
     of use: it is one 84px column and "1 stream · 1 block" does not fit in it.
     Three short lines fit inside the art's own 40px, so the row does not grow
     to pay for it. */
  .tv-n { flex: none; width: 84px; text-align: right; font-family: var(--font-mono);
    font-size: 0.64rem; line-height: 1.32; font-variant-numeric: tabular-nums;
    color: var(--text-3); }
  .tv-n i { display: block; font-style: normal; }
  .tv-n i.none { opacity: 0.55; }

  /* ── an open tag row ────────────────────────────────────────────────────
     Same shape as .nv-stream on this surface: the head keeps its own radius
     while shut and squares off against the body when open, so the two read as
     one card rather than as a row with a box under it. */
  .tv-tag { margin-bottom: 5px; }
  .tv-tag .tv-row { margin-bottom: 0; }
  .tv-tag.open > .tv-row { border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    background: var(--surface-2); }
  .tv-chev { flex: none; opacity: 0.5; transition: transform 0.18s ease; }
  .tv-tag.open .tv-chev { transform: rotate(180deg); }
  .tv-body { background: var(--panel); border: 1px solid var(--surface-2); border-top: 0;
    border-radius: 0 0 var(--radius) var(--radius); overflow: hidden; }
  .tv-tag:not(.open) > .tv-body { display: none; }
  .tv-rename { width: 100%; background: var(--surface); color: var(--text); font: inherit;
    font-weight: 600; border: 1px solid var(--accent); border-radius: 5px;
    padding: 1px 6px; outline: 0; }
  .tv-gate { flex: none; font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--accent); border-radius: 4px; padding: 1px 5px;
    border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); }

  .tv-top { display: grid; grid-template-columns: 112px 1fr; gap: 16px; padding: 15px 14px; }
  @media (max-width: 700px) { .tv-top { grid-template-columns: 1fr; } }
  .tv-art-big { position: relative; aspect-ratio: 3/4; border-radius: var(--radius-sm);
    overflow: hidden; background: var(--surface-2); display: grid; place-items: center; }
  .tv-art-big img { width: 100%; height: 100%; object-fit: cover; }
  .tv-art-big .ph { font-family: var(--font-display); font-size: 2rem; font-weight: 700;
    color: rgba(255,255,255,0.13); }
  .tv-art-big[data-tvart] { cursor: pointer; }
  .tv-art-big .drop { position: absolute; inset: 0; display: grid; place-items: center;
    background: rgba(20,18,25,0.74); opacity: 0; transition: opacity 0.15s;
    font-size: 0.64rem; color: var(--text-2); text-align: center; padding: 8px; }
  .tv-art-big:hover .drop, .tv-art-big.hot .drop { opacity: 1; }
  .tv-art-big.hot { outline: 2px dashed var(--accent); outline-offset: -3px; }
  .tv-art-big .drop b { display: block; color: var(--text); font-weight: 600; font-size: 0.68rem; }

  .tv-descrow { display: flex; align-items: flex-start; gap: 10px; }
  .tv-summ { flex: 1; min-width: 0; margin: 0; color: var(--text-2); font-size: 0.82rem;
    max-width: 66ch; border-left: 2px solid transparent; }
  /* Seeded text is drawn the way an approximate timestamp is: visibly not the
     same claim as something a person wrote. */
  .tv-summ.seeded { border-left-color: var(--seg-meta); padding-left: 10px;
    background: color-mix(in srgb, var(--seg-meta) 5%, transparent); }
  .tv-summ.edited { border-left-color: var(--surface-3); padding-left: 10px; }
  .tv-summ.empty { color: var(--text-3); font-style: italic; }
  .tv-summ.editable { border-radius: 4px; outline: 1px dashed transparent; }
  .tv-summ.editable:hover { outline-color: var(--surface-3); cursor: text; }
  .tv-state { display: block; font-family: var(--font-mono); font-size: 0.62rem; margin-top: 6px; }
  .tv-state.s { color: var(--seg-meta); }
  .tv-state.e { color: var(--text-3); }
  .tv-ta { width: 100%; background: var(--surface); color: var(--text); font: inherit;
    font-size: 0.82rem; border: 1px solid var(--surface-3); border-radius: 7px; padding: 7px 9px;
    resize: vertical; outline: 0; }

  .tv-ctl { display: flex; align-items: center; gap: 4px; flex: none; }
  .tv-sep { width: 1px; align-self: stretch; background: var(--surface-2); margin: 0 6px; }
  .tv-btn { display: inline-grid; place-items: center; width: 28px; height: 28px; flex: none;
    border-radius: 7px; border: 1px solid var(--surface-2); background: var(--surface);
    color: var(--text-2); cursor: pointer; transition: 0.13s; }
  .tv-btn:hover { background: var(--surface-2); color: var(--text); }
  .tv-btn[disabled] { opacity: 0.36; cursor: not-allowed; }
  .tv-btn.sprout:hover:not([disabled]) { color: #8fd694; border-color: rgba(143,214,148,0.4); }
  .tv-btn.bin:hover { color: var(--accent);
    border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
  /* The link out to whatever this was seeded from. An icon and nothing more:
     a url is 60 characters of noise next to a paragraph, and it is only ever
     read when somebody is about to go and look. */
  .tv-btn.lnk { color: var(--seg-character); text-decoration: none; }
  .tv-btn.lnk:hover { border-color: color-mix(in srgb, var(--seg-character) 45%, transparent); }
  /* The one press. Wide rather than an icon, because it is the thing you came
     to this row to do and an unlabelled sprout never said what it would ask. */
  .tv-look { display: inline-flex; align-items: center; gap: 6px; height: 28px; flex: none;
    padding: 0 10px; border-radius: 7px; border: 1px solid var(--surface-2);
    background: var(--surface); color: var(--text-2); cursor: pointer;
    font: inherit; font-size: 0.72rem; transition: 0.13s; }
  .tv-look:hover { background: var(--surface-2); color: var(--text); }
  .tv-look[disabled] { opacity: 0.5; cursor: progress; }
  /* Joined to the look-up, because it is a sentence: ask THIS catalogue.
     Two controls with a gap between them read as two unrelated verbs. */
  .tv-src { height: 28px; flex: none; padding: 0 9px; cursor: pointer;
    border: 1px solid var(--surface-2); border-right: 0;
    border-radius: 7px 0 0 7px; background: var(--surface); color: var(--text-3);
    font: inherit; font-size: 0.68rem; letter-spacing: 0.02em;
    transition: 0.13s; }
  .tv-src:hover, .tv-src[aria-expanded="true"] { background: var(--surface-2);
    color: var(--text); }
  /* The wrap exists so the menu has something to hang off; it sits between
     the button and the look-up, so the welded edge is addressed through it. */
  .tv-srcwrap { position: relative; display: inline-flex; flex: none; }
  .tv-srcwrap + .tv-look { border-radius: 0 7px 7px 0; }
  .tv-srcmenu { position: absolute; z-index: 40; top: calc(100% + 4px); left: 0;
    min-width: 190px; display: flex; flex-direction: column; padding: 4px;
    border-radius: 9px; border: 1px solid var(--surface-2);
    background: var(--panel); box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45); }
  .tv-srcmenu button { display: flex; flex-direction: column; gap: 1px;
    padding: 6px 9px; border: 0; border-radius: 6px; background: none;
    color: var(--text-2); font: inherit; text-align: left; cursor: pointer; }
  .tv-srcmenu button:hover { background: var(--surface-2); color: var(--text); }
  .tv-srcmenu button.on { color: var(--accent); }
  .tv-srcmenu b { font-size: 0.74rem; font-weight: 600; }
  /* The second line is why, not what — the label alone does not say that
     AniList will be asked about a person rather than a show. */
  .tv-srcmenu i { font-size: 0.64rem; font-style: normal; color: var(--text-3); }
  .tv-srcmenu hr { margin: 4px 6px; border: 0; border-top: 1px solid var(--surface-2); }

  /* ── the candidates ─────────────────────────────────────────────────────
     Under the description rather than in a popover: it is a list you read and
     compare, sometimes four rows of it, and a popover over the row you are
     deciding about would cover the name you are deciding for. */
  .tv-seed { margin-top: 10px; border-top: 1px dashed var(--surface-2); padding-top: 9px; }
  .tv-seed-note { font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-3);
    display: flex; align-items: center; gap: 7px; margin-bottom: 7px; }
  .tv-seed-note b { color: var(--text-2); font-weight: 600; }
  .tv-cand { display: flex; gap: 10px; width: 100%; text-align: left; padding: 7px 9px;
    border: 1px solid transparent; border-radius: 8px; background: var(--glass-sunk);
    color: var(--text-2); font: inherit; font-size: 0.78rem; cursor: pointer;
    margin-bottom: 5px; transition: 0.12s; }
  .tv-cand:hover, .tv-cand:focus-visible { border-color: var(--accent); outline: 0;
    background: var(--surface-2); color: var(--text); }
  .tv-cand img { width: 34px; height: 46px; flex: none; border-radius: 4px;
    object-fit: cover; background: var(--surface-3); }
  .tv-cand .noart { width: 34px; height: 46px; flex: none; border-radius: 4px;
    background: var(--surface-3); display: grid; place-items: center;
    font-family: var(--font-mono); font-size: 0.48rem; color: var(--text-3);
    text-align: center; line-height: 1.1; }
  .tv-cand-b { flex: 1; min-width: 0; }
  .tv-cand-h { display: flex; align-items: baseline; gap: 7px; }
  .tv-cand-n { font-weight: 600; color: var(--text); overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .tv-cand-m { font-family: var(--font-mono); font-size: 0.58rem; color: var(--text-3);
    flex: none; }
  /* EXACT and FUZZY are different CLAIMS and are drawn as different things.
     `where name =` is the archive asserting its tag IS the catalogue name;
     a search is a guess with a ranking, and a ranking is what hands you
     Pokemon GO when the tag said Pokemon. */
  .tv-cand-k { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.07em;
    text-transform: uppercase; flex: none; padding: 1px 5px; border-radius: 4px;
    background: var(--glass-2); color: var(--text-3); }
  .tv-cand-k.exact { color: var(--seg-character);
    background: color-mix(in srgb, var(--seg-character) 16%, transparent); }
  .tv-cand-s { color: var(--text-3); font-size: 0.72rem; line-height: 1.45;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden; }
  /* "None of these" is an ANSWER and not a cancel. Summer Camp is a Phase
     Connect event, and the catalogue answers it with a real game, a cover and
     a summary about finding Bigfoot — no amount of matching fixes that and
     only a person saying no does. */
  .tv-seed-foot { display: flex; gap: 6px; align-items: center; margin-top: 8px; }
  .tv-seed-foot button { border: 1px solid var(--surface-2); background: var(--surface);
    color: var(--text-2); border-radius: 7px; padding: 5px 10px; cursor: pointer;
    font: inherit; font-size: 0.72rem; }
  .tv-seed-foot button:hover { background: var(--surface-2); color: var(--text); }
  .tv-seed-foot input { flex: 1; min-width: 0; background: var(--glass-sunk);
    border: 1px solid var(--glass-line); border-radius: 7px; padding: 5px 9px;
    color: var(--text); font: inherit; font-size: 0.74rem; outline: 0; }
  .tv-seed-foot input:focus { border-color: var(--accent); }

  .tv-tabs { display: flex; gap: 2px; padding: 0 14px; border-bottom: 1px solid var(--surface-2); }
  .tv-tab { background: none; border: 0; border-bottom: 2px solid transparent;
    color: var(--text-3); font-family: inherit; font-size: 0.76rem; padding: 7px 11px;
    cursor: pointer; }
  .tv-tab.on { color: var(--text); border-bottom-color: var(--accent); }
  .tv-tab i { font-style: normal; font-family: var(--font-mono); font-size: 0.65rem;
    color: var(--text-3); margin-left: 5px; }
  .tv-uses { padding: 8px 14px 14px; display: grid; gap: 1px; }
  .tv-use { display: grid; grid-template-columns: 62px 1fr auto; gap: 12px; align-items: center;
    padding: 6px 8px; border: 0; border-radius: 7px; background: none; color: var(--text-2);
    font: inherit; font-size: 0.78rem; text-align: left; cursor: pointer; width: 100%; }
  .tv-use:hover { background: var(--surface); color: var(--text); }
  .tv-use .d { font-family: var(--font-mono); font-size: 0.66rem; color: var(--text-3); }
  .tv-use .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .tv-use .m { font-family: var(--font-mono); font-size: 0.64rem; color: var(--text-3); }
  .tv-use .strip { display: flex; height: 5px; border-radius: 3px; overflow: hidden; width: 104px; }
  .tv-use .strip i { display: block; height: 100%; }
  /* THE PICTURE HALF OF A TAG'S USES. `auto-fill` rather than a fixed count:
     this panel is the width of the index column on a desktop and the width of
     the screen on a phone, and a tag's tiles are the one thing here that
     should reflow rather than shrink. */
  .tv-uses.tv-grid { grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
    gap: 6px; align-items: start; }
  .tv-tile { position: relative; aspect-ratio: 1; padding: 0; overflow: hidden;
    border: 1px solid var(--surface-2); border-radius: var(--radius-sm);
    background: var(--surface); cursor: pointer; }
  .tv-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .tv-tile:hover { border-color: var(--accent); }
  /* A tile whose picture never arrived still has to be a square somebody can
     press -- it is a real row, and hiding it would make the grid lie about
     how many there are. */
  .tv-tile.noart::after { content: ''; position: absolute; inset: 0;
    background: var(--surface-2); }
  .tv-tile.noart img { display: none; }
  /* A MOVING ONE SAYS SO, for the reason the gallery tile does: a still frame
     of a gif and a still are the same picture until you press one. */
  .tv-moves { position: absolute; bottom: 2px; right: 3px; z-index: 1;
    font-size: 0.7rem; line-height: 1; font-style: normal; color: #fff;
    text-shadow: 0 0 3px rgba(0, 0, 0, 0.9); }
  .tv-pend { position: absolute; top: 2px; left: 3px; z-index: 1;
    font-family: var(--font-mono); font-size: 0.52rem; font-style: normal;
    padding: 1px 3px; border-radius: 3px; color: #fff;
    background: rgba(0, 0, 0, 0.6); }
  /* THE DOOR OUT, spanning the grid rather than sitting in it as a tile: it
     is not one of the pictures and must not read as one. */
  .tv-more { grid-column: 1 / -1; margin-top: 2px; padding: 6px 8px;
    font: inherit; font-size: 0.72rem; color: var(--text-2);
    background: none; border: 1px dashed var(--surface-2);
    border-radius: var(--radius-sm); cursor: pointer; }
  .tv-more:hover { color: var(--text); border-color: var(--accent); }
  .tv-loading { padding: 12px 8px; font-family: var(--font-mono); font-size: 0.66rem;
    color: var(--text-3); }

  .cal-vods { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 245px)); gap: 14px; }
  /* Tighter than a grid card, and the ridge has to know: it cancels
     `--body-pad-x` to reach the card's edge, so the override sets the variable
     rather than the padding. Setting only the padding here is what put the
     ridge 2px outside the card in this view, where `overflow: hidden` then
     clipped it to three pixels of the five. */
  .cal-vods .vod .body { --body-pad-x: 12px; padding: 11px var(--body-pad-x) 13px; }
  .cal-vods .vod .title { font-size: 0.86rem; }
  .cal-sentinel { padding: 26px 0 6px; text-align: center; font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }

  /* ── Controls under the calendar ── */
  .cal-controls { margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,0.06); display: flex; flex-direction: column; gap: 7px; }
  .cal-search { flex: 1 1 auto; }
  .cal-search input { padding: 9px 12px 9px 33px; font-size: 0.82rem; border-radius: 10px; }
  .cal-search svg { left: 11px; }
  .cal-toggle { display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px 11px; background: var(--surface); border: 0; border-radius: 10px; color: var(--text-2); font: inherit; font-size: 0.82rem; cursor: pointer; text-align: left; }
  .cal-toggle:hover { background: var(--surface-2); }
  /* ---- the month switch -------------------------------------------------
     A sunken track with a raised sliding knob, which is the one shape that
     says "this moves" without a caption. Built from the palette's own
     surfaces rather than the light greys the pattern usually ships with: a
     #ecf0f3 pill in this header would be a white slab, and the depth comes
     from the shadows, not from the lightness.
     THE BUTTON STOPS LOOKING LIKE A BUTTON. `.cal-toggle` is a filled pill
     and the track is now the filled thing, so a box inside a box is two
     backgrounds for one control. */
  .cal-toggle:has(.sw-knob) { background: none; padding-left: 0; }
  .cal-toggle:has(.sw-knob):hover { background: none; }
  .cal-toggle:has(.sw-knob):hover .sw-knob { color: var(--text-2); }
  .cal-toggle[aria-checked="true"]:has(.sw-knob):hover .sw-knob { color: var(--on-accent); }

  .sw { position: relative; flex-shrink: 0; width: 96px; height: 18px;
    border-radius: 9px; background: var(--surface); box-sizing: border-box;
    box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.42),
                inset -2px -2px 4px rgba(255, 255, 255, 0.045); }
  /* The number lives on the knob and the noun beside it, so the two always
     agree: ONE month, ALL months. `content` and not a text node because the
     state is already on the button — this is the visible echo of it, and
     `role="switch"` is what anything reading the page goes by. */
  .sw-knob { position: absolute; top: 0; left: 0; width: 48px; height: 18px;
    border-radius: 9px; box-sizing: border-box; background: var(--surface-2);
    color: var(--text-3); font-family: var(--font-mono); font-size: 0.56rem;
    font-weight: 700; letter-spacing: 0.06em; display: grid; place-items: center;
    box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.38),
                -1px -1px 3px rgba(255, 255, 255, 0.05);
    transition: transform 0.4s cubic-bezier(0.85, 0.05, 0.18, 1.35),
                background 0.25s ease, color 0.25s ease; }
  .sw-knob::after { content: 'ONE'; }
  /* THE PLURAL IS ALWAYS IN THE BOX, hidden rather than absent. `months` is
     wider than `month`, so swapping the word moved everything to its right
     every time the switch was pressed — and a control that shoves its own
     neighbours is the one thing a toggle must not do. `visibility` keeps the
     glyph's width and takes away its ink. */
  .sw-word i { font-style: normal; visibility: hidden; }
  .cal-toggle[aria-checked="true"] .sw-word i { visibility: visible; }
  .cal-toggle[aria-checked="true"] .sw-knob { transform: translate3d(48px, 0, 0);
    background: var(--accent); color: var(--on-accent); }
  .cal-toggle[aria-checked="true"] .sw-knob::after { content: 'ALL'; }
  /* And the hint only takes space when it has something to say — `archive`
     went, so in plain all-mode this is an empty box with a margin, which is
     the other half of the same shove. */
  .cal-toggle-hint:empty { display: none; }
  /* The travel is the whole affordance, so it is the one thing that stays
     when the knob's colours do not. */
  @media (prefers-reduced-motion: reduce) {
    .sw-knob { transition: transform 0.01s linear, background 0.25s ease; }
  }
  .cal-toggle-hint { margin-left: auto; font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-3); }
  /* Compact variant, for sitting in .cal-nav beside the month arrows. The hint
     shows only in all-mode: parked on a month, .cal-title one element to the
     left already says which one, and repeating it as "AUG ONLY" is noise. In
     all-mode the title becomes actively misleading, which is exactly when the
     word ARCHIVE has to be there. margin-left:auto is dropped with it — that
     only ever worked because the base button is width:100%. */
  .cal-toggle--inline { width: auto; gap: 7px; padding: 5px 9px; font-size: 0.76rem; }
  .cal-toggle--inline .cal-toggle-hint { display: none; margin-left: 2px; }
  .cal-toggle--inline[aria-checked="true"] .cal-toggle-hint { display: block; color: var(--accent); }
  /* ...unless there is nothing in it. Declared here rather than beside the
     switch because the rule above is more specific and later, so a plain
     `.cal-toggle-hint:empty` lost to it — and an empty block still spends the
     row's 7px gap and its own 2px margin, which is the whole of the shift
     this was meant to remove. */
  .cal-toggle--inline[aria-checked="true"] .cal-toggle-hint:empty { display: none; }

  /* ── Tag library ──────────────────────────────────────────────────────────
     Lives under the search in the 380px calendar column, which is fixed at
     every viewport above 560px — so the grid is sized to that, not to the
     page. Collapsed to a bar by default and fetched on first expand: the tag
     list is a request the index does not otherwise need to make.

     The card is deliberately the shape box art will want, not the shape the
     data currently justifies. There is no art yet, so it draws the same hued
     placeholder the stream cards use, with the tag's initial in it. When
     tag.thumb starts coming back non-null the <img> lands on top of the
     placeholder and nothing else moves. */
  .taglib { display: flex; flex-direction: column; }
  .taglib-bar { display: flex; align-items: center; gap: 8px; width: 100%;
    padding: 8px 11px; background: var(--surface); border: 0;
    border-radius: var(--radius-sm); color: var(--text-2); cursor: pointer;
    font: inherit; font-size: 0.78rem; text-align: left;
    transition: background 0.15s ease, color 0.15s ease; }
  .taglib-bar:hover { background: var(--surface-2); color: var(--text); }
  .taglib-label { font-weight: 600; }
  .taglib-chev { margin-left: auto; flex: none; transition: transform 0.18s ease; }
  .taglib-bar[aria-expanded="true"] .taglib-chev { transform: rotate(180deg); }
  .taglib-bar[aria-expanded="true"] { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
  /* The picked tag rides in the bar so it stays visible once the panel is
     collapsed again — otherwise the stream list is filtered by something with
     nothing on screen to say so. */
  .taglib-picked { display: inline-flex; align-items: center; gap: 5px;
    min-width: 0; max-width: 60%; padding: 2px 4px 2px 8px; border-radius: 999px;
    background: var(--accent-soft); color: var(--accent);
    font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.04em; }
  .taglib-picked b { font-weight: 500; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; min-width: 0; }
  .taglib-picked i { display: grid; place-items: center; width: 14px; height: 14px;
    border-radius: 50%; font-style: normal; font-size: 0.72rem; line-height: 1; flex: none; }
  .taglib-picked:hover i { background: var(--accent); color: var(--on-accent); }

  .taglib-body { background: var(--surface); border-radius: 0 0 var(--radius-sm) var(--radius-sm);
    padding: 10px; max-height: 328px; overflow-y: auto; scrollbar-width: thin; }
    /* Five across the 360px inner width, which lands the card at ~65px — about
     half the width it was. 58 and not 64: the band that yields five columns is
     a min of 53-64, so the top of it is one padding tweak away from becoming
     four. Middle of the band, not the edge. */
  .taglib-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(58px, 1fr)); gap: 9px; }
  .taglib-empty { padding: 6px 2px; font-size: 0.74rem; color: var(--text-3); }

  .tag-card { display: block; padding: 0; border: 0; background: transparent;
    font: inherit; text-align: left; cursor: pointer; min-width: 0; }
  .tag-card .tag-art { display: block; width: 100%; aspect-ratio: 3 / 4;
    border-radius: 7px; overflow: hidden; position: relative;
    box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06);
    background: linear-gradient(150deg,
      hsl(var(--hue) 34% 24%), hsl(calc(var(--hue) + 40) 28% 15%));
    transition: box-shadow 0.15s ease, filter 0.15s ease; }
  .tag-card:hover .tag-art { filter: brightness(1.14); }
  .tag-card .tag-art img { position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; }
  .tag-card .tag-initial { position: absolute; inset: 0; display: grid; place-items: center;
    font-family: var(--font-display); font-weight: 700; font-size: 0.98rem;
    color: rgba(255,255,255,0.3); }
  /* Kind, in the same four colours the timeline and the chips already use. */
  .tag-card .tag-kind { position: absolute; top: 4px; left: 4px; width: 6px; height: 6px;
    border-radius: 2px; transform: rotate(45deg); }
  .tag-card .tag-n { position: absolute; right: 3px; bottom: 3px;
    font-family: var(--font-mono); font-size: 0.5rem; font-variant-numeric: tabular-nums;
    color: var(--text-2); background: rgba(20,18,25,0.74); backdrop-filter: blur(4px);
    padding: 1px 4px; border-radius: 4px; }
  .tag-card .tag-name { display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 2; overflow: hidden; margin-top: 4px;
    font-size: 0.58rem; line-height: 1.24; min-height: 2.48em; color: var(--text-2); }
  .tag-card[aria-pressed="true"] .tag-art { box-shadow: inset 0 0 0 1px var(--accent), 0 0 0 2px var(--accent); }
  .tag-card[aria-pressed="true"] .tag-name { color: var(--accent); font-weight: 600; }

  /* Same hollow treatment as the add-stream card, sized to a tag tile. */
  .tag-card.tag-new .tag-art { background: none; box-shadow: none;
    border: 1px dashed rgba(255, 255, 255, 0.16); display: grid; place-items: center;
    transition: border-color 0.15s ease; }
  .tag-card.tag-new:hover .tag-art { border-color: var(--accent); filter: none; }
  .tag-card.tag-new .tag-ring { width: 22px; height: 22px; border-radius: 50%;
    border: 1px solid currentColor; display: grid; place-items: center;
    color: var(--text-3); opacity: 0.8; transition: color 0.15s ease; }
  .tag-card.tag-new:hover .tag-ring { color: var(--accent); opacity: 1; }
  .tag-card.tag-new .tag-name { color: var(--text-3); }
  .tag-card.tag-new:hover .tag-name { color: var(--accent); }

  .taglib-more { width: 100%; margin-top: 9px; padding: 6px; border: 0;
    background: var(--surface-2); border-radius: 7px; color: var(--text-2);
    font: inherit; font-size: 0.7rem; cursor: pointer; }
  .taglib-more:hover { background: var(--surface-3); color: var(--text); }

  /* ── Notes rail: follows the selected VOD ── */
  /* The notes rail belongs to peek. Outside it there is no stream selected and
     the panel is a 236px dashed placeholder holding 320px of the widest column
     on the page, so it collapses and .cal-streams — the only flex child that
     grows — takes the space back.
     Collapsed by width rather than display:none on purpose: this element is
     position:sticky, and toggling display on a sticky box mid-scroll snaps the
     layout instead of moving it. The negative margin eats .cal-layout's 30px
     gap, which would otherwise survive a zero-width child. */
  /* Only opacity animates. Transitioning the width looked smoother in theory and
     is worse in practice: the grid beside it re-lays-out on every frame of the
     0.26s, and grid track COUNTS do not interpolate — so the last card popped to
     a second row at whatever frame the width crossed the threshold, in the
     middle of everything else moving. Snapping the width means exactly one
     reflow, and the fade covers it. */
  .cal-notes { flex: 0 0 320px; width: 320px; position: sticky; top: 84px; align-self: flex-start;
    transition: opacity 0.18s ease; }
  @media (min-width: 561px) {
    .cal-layout:not(.notes-on) > .cal-notes {
      flex-basis: 0; width: 0; margin-left: -30px; opacity: 0;
      overflow: hidden; pointer-events: none; }
  }
  .notes-card { background: var(--surface); border-radius: var(--radius); padding: 14px; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05); }
  .notes-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 9px; text-align: center; min-height: 236px; padding: 22px 18px; border: 1px dashed rgba(255,255,255,0.09); border-radius: 10px; color: var(--text-3); }
  .notes-empty svg { opacity: 0.5; }
  .notes-empty-title { font-size: 0.82rem; font-weight: 600; color: var(--text-2); }
  .notes-empty-sub { font-size: 0.72rem; line-height: 1.5; max-width: 210px; }
  .nb-kicker { font-family: var(--font-mono); font-size: 0.56rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }
  .nb-title { font-family: var(--font-display); font-weight: 700; font-size: 1rem; line-height: 1.3; margin-top: 4px; }
  /* Scoped to the notes card on purpose. The theater rail uses the same class
     name and its own rule (.th-nb .nb-title) sets display:block for a
     single-line ellipsis; clamping through the bare .nb-title selector would
     make the two fight over `display` at a one-point specificity gap. */
  .cal-notes .nb-title { display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 2; overflow: hidden; }
  .nb-summary { font-size: 0.75rem; line-height: 1.55; color: var(--text-2); margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.06); }
  .nb-list-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin: 14px 0 6px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.06); }
  .nb-list-head span:first-child { font-family: var(--font-mono); font-size: 0.56rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }
  .nb-count { font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-3); font-variant-numeric: tabular-nums; }
  .nb-list { display: flex; flex-direction: column; gap: 1px; }
  .cal-notes .ob-line { font-size: 0.76rem; }

  .vod.selected { box-shadow: 0 0 0 1.5px var(--accent); }
  .vod.selected .title { color: var(--accent); }

  /* ---- Detail surface: non-blocking bottom sheet (lucide-style) ---- */
  .sheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: 50; background: var(--panel); border-radius: 18px 18px 0 0; box-shadow: 0 -12px 44px rgba(0,0,0,0.42); transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.22,1,0.36,1); }
  .sheet.open { transform: translateY(0); }
  .sheet-inner { max-width: 1080px; margin: 0 auto; padding: 22px 24px; display: flex; align-items: center; gap: 22px; position: relative; min-height: 128px; }
  .sheet-preview { flex: 0 0 auto; border-radius: 12px; }
  .sheet-preview.ratio-1 { width: 108px; height: 108px; }
  .sheet-preview.ratio-169 { width: 100%; aspect-ratio: 16/9; }
  .sheet-preview .glyph svg { width: 32px; height: 32px; }
  .sheet-badge { position: absolute; top: 8px; left: 8px; z-index: 1; }
  .sheet-main { flex: 1 1 auto; min-width: 0; padding-right: 44px; }
  .sheet-kicker { font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }
  .sheet-title { font-family: var(--font-display); font-weight: 700; font-size: 1.18rem; line-height: 1.25; margin: 5px 0 0; }
  .sheet-tags, .sheet-meta { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 9px; }
  /* ---- the meta line ------------------------------------------------------
     Three facts about one broadcast — which number, when, how long — and they
     were three filled pills, which drew them as three separate objects when
     they are one sentence. The fill goes and a separator carries the join.
     `.accent` IS EXEMPT. The open-suggestion pill is not part of that
     sentence; it is a notice that something is waiting, and a notice that
     looks like the text beside it is a notice nobody sees. */
  .sheet-meta .pill:not(.accent) { background: none; padding-left: 0;
    padding-right: 0; color: var(--text-2); }
  .sheet-meta .pill:not(.accent) + .pill:not(.accent)::before {
    content: '\2022'; margin-right: 7px; color: var(--text-3); }

  /* The actions sit on top of the thumbnail and share its width, so the left
     column is one object rather than a picture with buttons somewhere else.
     Watch here takes whatever is left after the two icons — which means it
     grows with the preview when a player is mounted. */
  /* The column carries the width and the preview fills it, so the button row
     and the thumbnail cannot drift apart — and the row grows with the preview
     when a player is mounted, instead of stranding three buttons above a
     560px video. 212px is what "Watch here" plus two icons actually needs. */
  .sheet-side { flex: 0 0 auto; width: 212px; display: flex; flex-direction: column; gap: 9px; }
  .sheet.playing .sheet-side { width: min(560px, calc(var(--vw) * 46)); }
  .sheet-actions { display: flex; gap: 6px; }
  .sheet-actions:empty { display: none; }
  /* Capped, because the column grows to 560px when a player mounts and 470px
     of solid pink is not a button, it is a wall. */
  .sheet-actions .btn-primary { flex: 1 1 auto; min-width: 0; max-width: 300px;
    padding-left: 8px; padding-right: 8px; white-space: nowrap; }
  /* "a little less than half" of the 118px these used to be. Two of them fit
     in the space one word-button was wasting. */
  /* 46px was two buttons wide; it is three now, and a square that reads as a
     glyph rather than as a slab leaves the primary action the room it wants. */
  .sheet-ico { flex: 0 0 34px; width: 34px; height: 34px; padding: 0;
    display: grid; place-items: center; }
  .sheet-ico svg { width: 15px; height: 15px; }
  /* THE PRIMARY WEARS THE SAME GLASS and keeps its colour, which is the whole
     brief: the row is one material now rather than a filled slab beside three
     ghosts. The accent is mixed rather than flat so the blur behind it still
     shows, and the radius comes up from 4 to the 10 the icons use — four
     buttons in a row that disagree about their corners read as two rows. */
  .sheet-actions .btn-primary { height: 34px; padding: 0 14px; border-radius: 10px;
    font-size: 0.82rem; color: var(--on-accent);
    background: color-mix(in srgb, var(--accent) 88%, transparent);
    -webkit-backdrop-filter: blur(12px) saturate(150%);
    backdrop-filter: blur(12px) saturate(150%);
    border: 1px solid color-mix(in srgb, #fff 24%, transparent);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18),
                inset 0 1px 0 color-mix(in srgb, #fff 20%, transparent);
    transition: transform 0.18s ease, background 0.18s ease, box-shadow 0.18s ease; }
  .sheet-actions .btn-primary:hover:not(:disabled) { transform: translateY(-1px);
    background: var(--accent);
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.26),
                inset 0 1px 0 color-mix(in srgb, #fff 28%, transparent); }
  .sheet-actions .btn-primary:active:not(:disabled) { transform: translateY(0) scale(0.97); }
  .sheet-actions .btn-primary:disabled { opacity: 0.4; cursor: not-allowed;
    box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 8%, transparent); }
  /* Both children are always present and CSS picks one. Swapping innerHTML
     inside the click handler detaches the very node the event came from, and
     the page's outside-click check then reads that node as "not in the sheet"
     and closes the sheet under you. Same trap as the tag popover. */
  .sheet-ico b { display: none; font-family: var(--font-mono); font-size: 0.5rem;
    font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--accent); }
  .sheet-ico.copied b { display: block; }
  .sheet-ico.copied svg { display: none; }

  /* The summary. Every stream's is empty, so the empty state is the common one
     and it has to invite rather than apologise. */
  .sum, .sum-empty { margin-top: 12px; font-size: 0.78rem; line-height: 1.55;
    color: var(--text-2); }
  .sum-t { display: block; white-space: pre-wrap; }
  .sum-empty { color: var(--text-3); font-style: italic; }

  /* ── inline editing ─────────────────────────────────────────────────────
     One outline, everywhere. It appears on hover and only for someone who can
     write, so the affordance answers "may I?" before you try — and the same
     rectangle marks the field while you are in it, so nothing jumps. */
  .editable { border-radius: 7px; box-shadow: 0 0 0 1px transparent;
    transition: box-shadow 0.12s ease, background 0.12s ease; }
  .editable:hover { box-shadow: 0 0 0 1px rgba(255,255,255,0.18); cursor: text; }
  .editable.editing { box-shadow: 0 0 0 1px var(--accent); background: var(--surface-2); }
  /* Pills already have a box; the outline sits on it rather than beside it. */
  .pill.editable { box-shadow: 0 0 0 1px transparent; }
  .pill.editable:hover { box-shadow: 0 0 0 1px rgba(255,255,255,0.22); }
  .inline-in { background: transparent; border: 0; padding: 0; margin: 0; color: var(--text);
    font: inherit; width: 100%; box-sizing: border-box; }
  .inline-in:focus { outline: none; }
  textarea.inline-in { resize: vertical; line-height: inherit; min-height: 3.4em; }
  .sheet-title .inline-in { font-family: var(--font-display); font-weight: 700; }
  /* The preview is an image; while it is being edited it is a text box, and it
     needs a floor of its own so the sheet does not jump. */
  .sheet-preview.editing { display: grid; place-items: center; padding: 0 10px;
    box-sizing: border-box; background: var(--surface-2); }
  .sheet-preview.editing .inline-in { font-family: var(--font-mono); font-size: 0.66rem; }

  /* vod / chat, drawn rather than spelled. The mark carries the colour. */
  .st-ico { display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 20px; border-radius: 6px; background: var(--surface-2);
    color: var(--text-3); }
  .st-ico svg { width: 13px; height: 13px; }
  .st-ico.ok { color: var(--accent); }
  /* Present but not playable. Brighter than `none`, nowhere near `ok`: there
     IS something on disk, and pretending otherwise would be as wrong as the
     accent was. */
  .st-ico.raw { color: var(--text-2); opacity: 0.8; }
  .st-ico.gone { color: #ffb3b3; }
  .st-ico.none { color: var(--text-3); opacity: 0.55; }

  /* ── card tools and the record editor ──────────────────────────────────
     Two corner controls, revealed on hover so a grid of 221 cards is not also
     a grid of 442 buttons. */
  .card-tools { margin-left: auto; display: flex; gap: 3px; opacity: 0;
    transition: opacity 0.12s ease; }
  .vod:hover .card-tools, .card-tools .btn-ent:focus-visible { opacity: 1; }
  /* THE GATE ABOVE stays because every control in this strip is an editor's
     — a wrench, a delete, a history — and a grid of 221 broadcasts should
     look like 221 broadcasts rather than 663 buttons.

     AND THE ONE SIZE IN THE FAMILY THAT IS NOT 30px, which is measured and
     not a preference. These three do not sit in a row of their own: they
     share `.sub` with the date and the duration pill, and that line is 22px
     tall because `.pill` is. A 30px button makes it 30 — the card's whole
     lower half jumps on hover, which is the one moment the eye is already on
     it. The box matches the pill it stands next to; the icon comes down with
     it, because 15px inside 22px is an icon in a box that is not there. */
  .card-tools .btn-ent { width: 22px; height: 22px; border-radius: 6px; }
  .card-tools .btn-ent svg { width: 13px; height: 13px; }
  /* The add-stream card: a grid item like any other, so it inherits the row's
     height instead of needing one, but hollow — dashed outline, no fill, and a
     plus in a ring where the thumbnail would be. It carries #cal-new, the id
     the old header button had, so the open handler and the record popover's
     ignore-list keep working untouched. For anybody who can propose a change;
     renderWho() puts it in and takes it out. */
  /* THE CARD-SHAPED ONE, and the odd member on purpose. The others are a
     pill or a square beside the set they extend; this is a whole cell the
     size of the stream cards around it, so it reads as the next card rather
     than as a control near them. The faint fill is its own — a cell with no
     fill at all reads as a hole in the grid rather than as a slot. */
  .vod-add { display: grid; place-items: center; min-height: 200px; padding: 0;
    border-radius: var(--radius); background: rgba(255, 255, 255, 0.012); }
  .vod-add:hover, .vod-add:focus-visible {
    color: var(--accent); border-color: var(--accent);
    background: rgba(255, 255, 255, 0.03); }
  .vod-add-ring { width: 44px; height: 44px; border-radius: 50%;
    border: 1px solid currentColor; display: grid; place-items: center;
    opacity: 0.75; transition: opacity 0.15s ease; }
  .vod-add:hover .vod-add-ring { opacity: 1; }
  .head-tools { margin-left: auto; display: flex; gap: 4px; }
  .head-tool { border: 1px dashed rgba(255,255,255,0.18); background: transparent;
    color: var(--text-3); width: 22px; height: 22px; border-radius: 999px; cursor: pointer;
    display: grid; place-items: center; font-size: 0.9rem; line-height: 1; padding: 0; }
  .head-tool:hover { border-color: var(--accent); color: var(--accent); }

  /* ── the record editor ───────────────────────────────────────────────────
     Same vocabulary as the upload window, because it is the same kind of
     thing: a form you fill in over the top of the page. Two rules carry most
     of it — the label sits ABOVE its control, and controls are pressed INTO
     the panel rather than raised off it.

     The label used to sit in a 116px gutter beside the input, which left about
     250px for a url, a media-root path or a stream title. Moving it up is what
     buys the width; the panel going 380 -> 424 is the smaller half of it.

     Every selector here is scoped to .pop5 or namespaced .rec-*. Twice now a
     panel has borrowed a bare class name — `.who`, then `.tq-state` — and
     repainted a panel nobody was looking at. */
  .rec-body { margin-top: 6px; }

  /* One field: its label, then its control, full width. */
  .rec-f { margin-bottom: 10px; }
  .rec-lb { display: block; margin-bottom: 4px; font-family: var(--font-mono);
    font-size: 0.6rem; letter-spacing: 0.05em; text-transform: uppercase;
    color: var(--text-3); }
  /* Fields that answer one question, side by side. When a stream started, how
     long it ran and which zone those are read in are three parts of one
     reading, and they are checked against each other rather than in turn. */
  .rec-grid { display: flex; gap: 8px; align-items: flex-start; }
  .rec-grid > .rec-f { flex: 1 1 0; min-width: 0; }
  .rec-grid > .rec-f.narrow { flex: 0 0 78px; }
  .rec-grid > .rec-f.tiny { flex: 0 0 62px; }
  .rec-grid > .rec-f.wide { flex: 3 1 0; }
  .rec-grid > .rec-f.rec-pico { flex: 0 0 46px; }
  /* The box, in the top row between the id and the title. Its own width
     rather than a share of the row: a checkbox is one glyph wide and the
     title wants everything that is left. The inner span matches the height of
     a field beside it so the three sit on one baseline — the label above is
     the same `.rec-lb` the fields use, which is what makes it read as one of
     them rather than as a control bolted into their row. */
  .rec-grid > .rec-f.rec-cert { flex: 0 0 48px; }
  .rec-cert-in { display: flex; align-items: center; height: 32px; }
  /* THE BOX SHOWS WHAT IT STAMPS. A tick would have said "on"; the mark says
     what turning it on puts on the card, so the control and its result are
     the same picture and nothing has to be learned twice.
     Pressed INTO the panel when it is off, which is the rule the rest of this
     form follows, and filled when it is on — the same accent/on-accent pair
     the gallery's pick tick and the shelf track's All already use. The native
     input stays for the keyboard and the change event; only its drawing is
     ours. */
  .rec-cert input { position: absolute; width: 0; height: 0; opacity: 0;
    pointer-events: none; }
  .rec-cert-box { width: 24px; height: 24px; border-radius: 7px; display: grid;
    place-items: center; cursor: pointer; color: transparent;
    background: var(--glass-sunk); border: 1px solid var(--border);
    transition: background 0.14s ease, border-color 0.14s ease, color 0.14s ease; }
  .rec-cert-box svg { width: 15px; height: 15px; display: block; }
  .rec-cert:hover .rec-cert-box { border-color: var(--text-3); }
  .rec-cert input:checked + .rec-cert-box { background: var(--accent);
    border-color: transparent; color: var(--on-accent); }
  .rec-cert input:focus-visible + .rec-cert-box { outline: 1px solid var(--accent);
    outline-offset: 2px; }

  /* A row whose label IS its glyph, on the same line as the control. A word
     stacked above a box that already holds a recognisable path spends a line
     of height saying what the box says. Used for the two file paths, which are
     the only fields here that name something on disk. */
  .rec-inline { display: flex; align-items: center; gap: 9px; }
  .rec-ico { flex: none; display: grid; place-items: center; color: var(--text-3); }
  .rec-ico svg { display: block; width: 17px; height: 17px; }
  .rec-inline .rec-pick { flex: 1 1 auto; min-width: 0; }

  /* The select sits ON the glyph, invisible and full-size, so the thing you
     click is the icon and the thing that opens is the browser's own menu.
     `appearance: none` kills the arrow a zero-opacity select would still
     reserve width for in some engines. The width lives up with the other
     `.rec-grid` overrides, because a bare `.rec-plat`-style class loses to
     `.rec-grid > .rec-f` on specificity and the cell stretches. */
  .rec-pico { position: relative; display: grid; place-items: center;
    min-height: 34px; border-radius: 8px; background: var(--glass-sunk); }
  .rec-pico select { position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; cursor: pointer; appearance: none; -webkit-appearance: none;
    border: 0; padding: 0; }
  .rec-pico .rec-ico { color: var(--text-2); pointer-events: none; }
  .rec-pico .rec-ico svg { width: 19px; height: 19px; }
  /* Focus has to be visible on the WRAPPER: the element actually focused is
     the invisible select, and an outline on something at zero opacity is an
     outline nobody can see. */
  .rec-pico:focus-within { outline: 2px solid var(--air); outline-offset: 1px; }
  /* Filled by the picker, never typed. A path that does not exist on disk is a
     state nothing else in this form can produce, and the browse panel is right
     there — so the box shows the answer rather than inviting a new one. */
  .pop5 input[readonly] { color: var(--text-2); cursor: default; }
  .pop5 input[readonly]:focus { border-color: var(--glass-line); }

  /* Bottom-aligned, because the fields beside it carry a label above and this
     has nothing to say — a button with a blank label over it is worse than a
     button that simply lines up with the boxes. */
  .rec-btn { align-self: flex-end; flex: none; display: grid; place-items: center;
    /* 31px, which is what a .pop5 input measures — 0.78rem of line box plus
       7px of padding and a hairline each side. Matched by hand because the
       button has no text to size it. */
    width: 34px; height: 31px; margin-bottom: 10px; padding: 0;
    border: 1px solid var(--glass-line); border-radius: 9px;
    background: var(--glass-2); color: var(--text-2); cursor: pointer; }
  .rec-btn:hover { border-color: var(--accent); color: var(--accent); }
  .rec-btn svg { display: block; width: 15px; height: 15px; }

  /* One line: what it is, a separator, what it says. It is read, never set, so
     it does not get the shape of something you fill in. */
  .rec-plat { display: flex; align-items: baseline; gap: 7px; margin-bottom: 10px; }
  .rec-plat .rec-lb { margin: 0; }
  .rec-plat i { font-style: normal; color: var(--text-3); }
  .rec-plat b { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500;
    color: var(--text-2); }
  .rec-plat b.none { color: var(--text-3); font-style: italic; font-weight: 400; }

  /* Pressed in, not raised — the .up-win input, restated for this panel. It
     has to out-specify `.pop2 input`, which is declared later in this sheet
     and would otherwise win on source order at equal specificity. */
  .pop5 input[type=text], .pop5 input:not([type]), .pop5 select {
    width: 100%; box-sizing: border-box; background: var(--glass-sunk);
    border: 1px solid var(--glass-line); color: var(--text); font-family: inherit;
    font-size: 0.78rem; padding: 7px 9px; border-radius: 9px; }
  .pop5 input:focus, .pop5 select:focus { outline: none; border-color: var(--accent);
    background: var(--glass-sunk); }
  .pop5 input::placeholder { color: var(--text-3); }
  .rec-hint { display: block; margin: 4px 0 0; font-size: 0.62rem; color: var(--text-3);
    line-height: 1.45; }
  .rec-grid + .rec-hint { margin: -6px 0 10px; }
  /* A hint explains the field above it, so the next field has to start further
     down than the hint's own line height or the two read as one block. */
  .rec-hint + .rec-f { margin-top: 11px; }
  /* Unless there is no hint. The fetch status line is an empty span until the
     recorder answers, and an empty explanation still collected its 4px and
     handed the field below it 11px of room to clear text that is not there. */
  .rec-hint:empty { display: none; margin: 0; }
  /* A hint that is a WARNING and not an explanation — the only one in this
     family that has to be read rather than skimmed. It says a published
     torrent is pointing at bytes that are no longer on disk. */
  .rec-hint.is-warn { color: var(--warn, #f5a524); }
  .rec-hint:empty + .rec-f { margin-top: 0; }

  /* A grid of boxes followed by a grid of labels is the one place in this
     panel where two rows meet tightly: the field above ends on a hard border
     and the row below opens with a 9.6px uppercase label whose own leading is
     about 2px, so the 10px between them reads as about half that. Grid to
     grid only -- a field following a field is already fine. */
  .rec-grid + .rec-grid { margin-top: 6px; }

  /* A value that came from somewhere else. Shown so the derivation can be
     checked at a glance, and not shown as a box so nobody types into it. */
  /* Pulled up under the control it describes. A read-only line is a caption
     for the field ABOVE it, and at the default spacing it sat exactly halfway
     between two fields and read as a label for the wrong one. */
  /* Tucked under the box it was derived from, and NOT on top of it: -6px put
     the line across the input's bottom border, which reads as a rendering
     fault rather than as a caption. The room below is its own margin now,
     because the rule that used to provide it — `.rec-ro + .rec-f` — matched
     nothing: `remote_id` renders as a bare hidden input, so the next element
     after this line is not a field at all and the label under it collided
     with the box above. A margin that depends on what happens to come next is
     a margin that stops working the day something is inserted. */
  .rec-ro { display: block; margin: 2px 0 11px; font-family: var(--font-mono);
    font-size: 0.6rem; color: var(--text-3); overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .rec-ro b { color: var(--text-2); font-weight: 500; }
  .rec-ro.none { font-style: italic; opacity: 0.75; }

  /* The section rule, borrowed from .up-qh: the words, then a hairline out to
     the edge. Cheaper than a box and it does not nest. */
  /* ---- the audit report, inside the record panel -------------------------
     The CLI sets these in aligned columns because it has eighty of them. This
     panel has about four hundred pixels, so the same facts stack: one line
     naming the thing, one or two lines of numbers under it. Mono for the
     numbers only — they are read by comparison, digit under digit — and prose
     weight for everything a person reads as a sentence. */
  .au-head { display: flex; align-items: baseline; gap: 7px; font-size: 0.8rem;
    font-weight: 600; margin-bottom: 8px; }
  /* Its own colours rather than `.pill .mk`'s: those are scoped to a pill and
     spell a different vocabulary (present / lost / never), and borrowing the
     selector would mean this line silently losing its colour the day the pill
     changes. */
  .au-head .mk { font-weight: 700; color: var(--text-3); }
  .au-head .mk.ok { color: var(--good, #30a46c); }
  .au-head .mk.warn { color: var(--warn, #f5a524); }
  .au-head .mk.bad { color: var(--bad, #e5484d); }
  .au-dim { color: var(--text-3); font-weight: 400; font-family: var(--font-mono);
    font-size: 0.66rem; }
  .au-p { margin: 0 0 8px; padding: 7px 9px; border-radius: 8px;
    background: var(--glass-sunk); }
  .au-id { font-size: 0.74rem; color: var(--text-2);
    overflow-wrap: anywhere; }
  .au-id b { color: var(--text); font-family: var(--font-mono);
    font-size: 0.66rem; margin-right: 6px; }
  .au-n { font-family: var(--font-mono); font-size: 0.66rem; color: var(--text-3);
    margin-top: 3px; overflow-wrap: anywhere; }
  /* The verdicts. Every check gets a chip so a check that silently stopped
     running cannot look like one that passed. */
  .au-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
    margin: 6px 0; }
  .au-row > b { font-family: var(--font-mono); font-size: 0.63rem;
    color: var(--text-3); margin-right: 2px; }
  .au-c { padding: 1px 6px; border-radius: 5px; font-family: var(--font-mono);
    font-size: 0.62rem; background: var(--glass-sunk); color: var(--text-3);
    border: 1px solid var(--border); }
  .au-c.ok { color: var(--good, #30a46c);
    border-color: color-mix(in srgb, var(--good, #30a46c) 40%, transparent); }
  .au-c.warn { color: var(--warn, #f5a524);
    border-color: color-mix(in srgb, var(--warn, #f5a524) 45%, transparent); }
  .au-c.bad { color: var(--bad, #e5484d);
    border-color: color-mix(in srgb, var(--bad, #e5484d) 45%, transparent); }
  /* And the sentence, which is where a problem's whole value is. */
  .au-say { font-size: 0.73rem; color: var(--text-2); margin: 5px 0;
    padding-left: 9px; border-left: 2px solid var(--border); }
  .au-say b { font-family: var(--font-mono); font-size: 0.63rem;
    color: var(--text-3); }
  .au-say.warn { border-left-color: var(--warn, #f5a524); }
  .au-say.bad { border-left-color: var(--bad, #e5484d); }

  /* ── A SECTION TITLE, and one definition of it ─────────────────────────
     The mono-caps label with a rule running off to the right is what this page
     means by a section title. It was written twice -- here for the record
     editor's blocks and once as `.al-sub`, which the alias manager used for
     its two tables before they became one list
     -- which is how two things meant to look identical come to disagree. One
     selector list now, so a change to the shape reaches every one of them.
     `.al-sub` is an `h3`, hence the explicit size and weight: the heading
     defaults would otherwise outrank nothing and simply win. */
  .rec-sec, .al-sub { display: flex; align-items: center; gap: 8px; margin: 17px 0 9px; }
  .rec-sec span, .al-sub { font-family: var(--font-mono); font-size: 0.63rem;
    font-weight: 500; letter-spacing: 0.05em; color: var(--text-3);
    text-transform: uppercase; }
  .rec-sec::after, .al-sub::after { content: ""; flex: 1; height: 1px;
    background: var(--glass-line); }

  /* pills — the default source. `auto` is a real choice, not the absence of
     one, so it gets a pill like the others rather than an empty text box. */
  .rec-pills { display: flex; gap: 4px; min-width: 0; }
  .rec-pills button { flex: 1 1 auto; font-family: var(--font-mono); font-size: 0.58rem;
    padding: 6px; border: 1px solid var(--glass-line); border-radius: 9px;
    background: var(--glass-sunk); color: var(--text-2); cursor: pointer; }
  .rec-pills button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent);
    background: color-mix(in oklab, var(--accent) 12%, transparent); }

  /* file pickers */
  .rec-pick { position: relative; display: flex; gap: 5px; min-width: 0; }
  .rec-pick input { flex: 1 1 auto; min-width: 0; padding-right: 26px; }
  /* Inside the box, at its right edge — it belongs to the value, not to the
     row, and a third button in the row would read as a third thing you can do
     rather than as "undo the one that is showing". Hidden when there is
     nothing to clear, because an × over an empty box is a dead control. */
  .rec-clear { position: absolute; right: 46px; top: 50%; transform: translateY(-50%);
    width: 18px; height: 18px; display: grid; place-items: center; padding: 0;
    border: 0; border-radius: 5px; background: none; color: var(--text-3);
    font-size: 0.8rem; line-height: 1; cursor: pointer; }
  .rec-clear:hover { background: var(--glass-3); color: var(--text); }
  .rec-clear[hidden] { display: none; }
  .rec-pick button { flex: 0 0 auto; padding: 0 11px; border: 1px solid var(--glass-line);
    border-radius: 9px; background: var(--glass-2); color: var(--text-2); cursor: pointer; }
  .rec-pick button:hover { border-color: var(--accent); color: var(--accent); }
  /* A fixed width, so the clear button's offset below is a number rather than
     a guess: 34 plus the 5px gap is what `.has-grab` adds to it. */
  .rec-grab { width: 34px; padding: 0; display: grid; place-items: center; }
  .rec-grab svg { width: 14px; height: 14px; }
  .rec-grab:disabled { opacity: 0.5; cursor: default;
    border-color: var(--glass-line); color: var(--text-3); }
  /* The clear x lives inside the input, so its offset from the right edge has
     to clear every button standing to the right of it. */
  .rec-pick.has-grab .rec-clear { right: 85px; }
  .rec-browse { margin: 5px 0 0; border: 1px solid var(--glass-line); border-radius: 9px;
    background: rgba(0,0,0,0.2); overflow: hidden; }
  .rec-browse-bar { display: flex; align-items: center; gap: 6px; padding: 5px 7px;
    border-bottom: 1px solid var(--glass-line); }
  .rec-browse-dir { flex: 1 1 auto; font-family: var(--font-mono); font-size: 0.55rem;
    color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .pop5 .rec-browse-q { flex: 0 0 110px; font-size: 0.62rem; padding: 4px 7px; }
  .rec-browse-list { max-height: 190px; overflow-y: auto; }
  .rec-browse-list button { display: flex; width: 100%; gap: 8px; text-align: left;
    padding: 5px 9px; border: 0; background: none; color: var(--text-2);
    font-size: 0.68rem; cursor: pointer; }
  .rec-browse-list button:hover { background: var(--glass-3); color: var(--text); }
  .rec-browse-list button > em { flex: 0 0 auto; margin-left: auto; font-style: normal;
    font-family: var(--font-mono); font-size: 0.55rem; color: var(--text-3); }
  /* The bottom of a page, while the next one is on its way. It is a row in the
     list rather than a spinner over it, so the rows you were reading do not
     move when it arrives. */
  .rec-browse-more { padding: 6px 9px; font-family: var(--font-mono);
    font-size: 0.55rem; letter-spacing: 0.05em; color: var(--text-3); }
  /* Uploading one, above the list of ones that are already here. A separate
     act from picking, so it is separated by a rule rather than sitting in the
     list as a row that behaves differently from its neighbours. */
  .rec-browse-up { display: flex; align-items: center; gap: 8px; padding: 7px 9px;
    border-bottom: 1px solid var(--glass-line); font-size: 0.68rem; color: var(--text-3); }
  .rec-browse-up button { flex: none; border: 1px solid var(--glass-line);
    border-radius: 8px; background: var(--glass-2); color: var(--text-2);
    font: inherit; font-size: 0.66rem; padding: 4px 9px; cursor: pointer; }
  .rec-browse-up button:hover { border-color: var(--accent); color: var(--accent); }
  .rec-browse-up button:disabled { opacity: 0.5; cursor: default; }
  .rec-browse-up.sad { color: #ffb3b3; }

  /* A capture, as an .up-item: a head that can be read without opening it, and
     a body that stays shut until you want it. A stream with three captures put
     twenty-one inputs between the top of this form and its Save button. */
  .rec-cap { margin-top: 8px; background: var(--glass-2);
    border: 1px solid rgba(255,255,255,0.055); border-radius: 11px; padding: 9px 10px; }
  .rec-cap-head { display: flex; align-items: center; gap: 9px; width: 100%;
    border: 0; background: none; padding: 0; margin: 0; color: inherit;
    text-align: left; font: inherit; cursor: pointer; }
  /* The platform is a picture here now, not two letters. A glyph does not
     need a pill around it to be read, and the pill this wore was sized to
     mono text that is no longer in it — an icon with a viewBox and no
     width/height took the browser's default size inside it, which is the
     empty rounded box this used to render as. The text properties stay for a
     platform code PLAT_ICO does not know: those still arrive as characters,
     so the slot grows past its 20px rather than clipping them. */
  .rec-cap-plat { flex: none; display: grid; place-items: center;
    min-width: 20px; height: 20px; color: var(--text-2);
    font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.06em; }
  .rec-cap-plat svg { display: block; width: 17px; height: 17px; }
  .rec-cap-mid { flex: 1 1 auto; min-width: 0; }
  /* Block, not inline: a real url is sixty characters and the state line has
     to survive it. An inline span ignores the overflow rules and the state
     simply falls off the right of the card. */
  .rec-cap-mid { display: block; }
  .rec-cap-name { display: block; font-size: 0.76rem; font-weight: 600;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Lowercase, no exclamation — the upload window's state line, same voice. */
  .rec-cap-state { display: block; font-size: 0.67rem; color: var(--text-3);
    margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .rec-chev { flex: none; color: var(--text-3); transition: transform 0.2s ease; }
  .rec-cap.shut .rec-chev { transform: rotate(-90deg); }
  .rec-cap.shut .rec-cap-body { display: none; }
  .rec-cap-body { margin-top: 10px; padding-top: 10px;
    border-top: 1px solid var(--glass-line); }
  /* Removing a capture is the one irreversible thing on this form, so it sits
     at the bottom of a card you had to open, rather than beside the name. */
  .rec-capdel { border: 0; background: transparent; cursor: pointer;
    font-family: var(--font-mono); font-size: 0.55rem; text-transform: uppercase;
    letter-spacing: 0.06em; padding: 4px 0 0; }
  .toast-act { margin-top: 7px; border: 0; background: var(--surface-3); color: var(--text);
    border-radius: 7px; padding: 5px 12px; cursor: pointer; font-family: var(--font-mono);
    font-size: 0.58rem; letter-spacing: 0.06em; text-transform: uppercase; }
  .toast-act:hover { background: var(--accent); color: var(--on-accent); }

  /* The read-only strip. Same colours and the same tiling as the theater's, at
     half the height — it is here to answer "what is in this" at a glance, not
     to be worked on, so nothing about it invites a click. */
  .sheet-strip-head { display: flex; align-items: baseline; gap: 10px; margin-top: 15px;
    font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.09em;
    text-transform: uppercase; color: var(--text-3); }
  .sheet-strip-head .navhint { margin-left: auto; }
  .sheet-strip { position: relative; height: 15px; margin-top: 5px; border-radius: 4px;
    overflow: hidden; background: rgba(255,255,255,0.05); cursor: default; }
  .sheet-strip i { position: absolute; top: 0; bottom: 0; display: flex; align-items: center;
    padding: 0 5px; overflow: hidden; }
  .sheet-strip i + i { box-shadow: -2px 0 0 var(--panel); }
  .sheet-strip i span { font-family: var(--font-mono); font-size: 0.47rem; letter-spacing: 0.05em;
    text-transform: uppercase; white-space: nowrap; color: rgba(20,18,25,0.78); font-weight: 600; }
  .sheet-strip i.unknown { background-image: repeating-linear-gradient(45deg,
      rgba(255,255,255,0.055) 0 5px, rgba(255,255,255,0) 5px 10px); }
  .sheet-strip i.unknown span { color: var(--text-3); }
  .sheet-close { position: absolute; top: 16px; right: 20px; background: var(--surface-2); border: 0; color: var(--text-2); width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; }
  .sheet-close:hover { background: var(--surface-3); color: var(--text); }
  /* Measured, not guessed. The sheet grew a strip and a button row this week
     and a hardcoded 190px quietly parked the last row of cards underneath it.
     --sheet-h is written by fitSheet() whenever the sheet changes size. */
  body.sheet-open main { padding-bottom: calc(var(--sheet-h, 190px) + 26px); }

  @media (max-width: 1520px) { .cal-vods { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
  @media (max-width: 1240px) { .cal-vods { grid-template-columns: repeat(2, minmax(0, 1fr)); } .cal-notes { flex-basis: 268px; width: 268px; } }
  @media (max-width: 560px) {
    :root { --gutter: 18px; }
    .brand-eyebrow { display: none; }
    .tabs { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; }
    .clip { flex-wrap: wrap; }
    /* desktop-first for now; this just keeps small screens from breaking */
    .cal-layout { flex-direction: column; gap: 22px; }
    .calendar-main { flex: 1 1 auto; width: 100%; }
    .cal-pop { position: fixed; left: 12px !important; right: 12px; top: auto !important; bottom: 12px; width: auto; }
    .cal-pop::before { display: none; }
    .cal-vods { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .cal-notes { position: static; flex: 1 1 auto; width: 100%; }
    .sheet-inner { flex-wrap: wrap; gap: 14px; }
    /* The close button is absolutely positioned top-right and lands on the
       plane icon once the row spans the sheet. Give it its corner back. */
    .sheet-side, .sheet.playing .sheet-side { width: 100%; padding-right: 40px;
      box-sizing: border-box; }
    .sheet-main { padding-right: 0; width: 100%; }
    /* The two icons keep their size; only Watch here takes the slack. */
    .sheet-actions .btn-primary { flex: 1 1 auto; min-width: 0; }
  }
  @media (prefers-reduced-motion: reduce) { *, .tab-indicator { transition: none !important; animation: none !important; } }

  /* ── additions for live data ─────────────────────────────────────────────
     Everything above this line is the mockup's CSS, unchanged. */

  /* Real thumbnails. The image sits over the .ph placeholder rather than
     replacing it, so a dead ytimg URL falls back to the glyph by itself. */
  .thumb img, .sheet-preview img { position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; z-index: 0; }
  .thumb img[hidden] { display: none; }
  /* Hue derived from the stream id, so a wall of placeholders reads as
     distinct cards instead of one grey slab. */
  .ph[style*="--hue"]::before { content: ""; position: absolute; inset: 0;
    background: linear-gradient(150deg,
      hsl(var(--hue) 30% 18%) 0%, hsl(calc(var(--hue) + 40) 26% 12%) 100%); }

  /* Player. The sheet grows only while something is playing. */
  .sheet-preview iframe { position: absolute; inset: 0; width: 100%; height: 100%;
    border: 0; border-radius: 12px; z-index: 1; }
  .sheet-src { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 9px; }
  .sheet-src button { display: inline-flex; align-items: center; gap: 5px;
    font-family: var(--font-mono); font-size: 0.62rem; padding: 3px 8px;
    border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: transparent;
    color: var(--text-2); cursor: pointer; }
  /* The platform marks. Sized to the type beside them rather than to a grid,
     because on a `[YT] — local` button the glyph and the word are one label
     and a mark that overpowers its own caption reads as a second control. */
  .sheet-src button svg { width: 13px; height: 13px; flex: none; }
  .sheet-src button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
  .sheet-src button:disabled { opacity: 0.4; cursor: not-allowed; }

  /* Timestamps are links now, not clipboard buttons. */
  a.ob-ts { text-decoration: none; }
  .ob-ts.approx::after { content: "~"; opacity: 0.6; }
  .ob-line.playing .ob-text { color: var(--accent); }

  /* Connection state. Silence is the wrong answer when the API is down. */
  .banner { margin: 10px 0 0; padding: 9px 12px; border-radius: 9px; font-size: 0.74rem;
    background: rgba(255,120,120,0.09); color: #ffb3b3;
    border: 1px solid rgba(255,120,120,0.2); }
  .banner[hidden] { display: none; }
  /* ---- the password you were handed ------------------------------------
     A DEBT, not an error, so it is not the red banner above. The door catches
     the common case on the way in; this is for a session that is already open
     — a tab reopened, a cookie that outlived the window — which lands in the
     archive with every press about to be refused. The form is IN the bar
     rather than behind a link, because there is exactly one thing to do and
     making somebody navigate to do it is how they end up pressing other
     things first and collecting refusals. */
  .pwbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 10px 14px; font-size: 0.76rem; color: var(--text);
    background: var(--accent-soft); border-bottom: 1px solid var(--accent); }
  .pwbar[hidden] { display: none; }
  .pwbar b { font-weight: 600; }
  .pwbar input { flex: 0 1 190px; min-width: 0; font: inherit; font-size: 0.76rem;
    padding: 6px 9px; border-radius: 7px; color: var(--text);
    background: var(--surface-2); border: 1px solid var(--border); }
  .pwbar input:focus { border-color: var(--accent); outline: none; }
  .pwbar .cs-btn { flex: none; }
  .pwbar .pw-why { flex: 1 1 260px; min-width: 0; color: var(--text-2);
    font-size: 0.72rem; line-height: 1.5; }
  .skeleton { opacity: 0.45; pointer-events: none; }

  /* ═══════════════════════════════════════════════════════════════════════
     LAYER 3 — theater
     Namespaced .th-* throughout so nothing collides with .sheet-* / .cal-* /
     .vod / .ob-*.
     ═══════════════════════════════════════════════════════════════════════ */
  :root {
    /* Segment colour is per CATEGORY, not per segment: two zatsudan blocks in
       one stream are the same swatch, and they stay that swatch across every
       stream in the archive. Fixed order, never cycled.

       Grey is reserved for time that is not content — waiting screen, break,
       outro — so a glance tells you how much of a 4h VOD is worth opening.
       `unknown` is deliberately NOT a colour: it is a hatch, because nobody
       has said what that stretch is and a swatch would be a claim. That also
       gives the strip its secondary encoding, which the palette needs.

       Validated against #1b1924 (dataviz validator, --pairs all): every pair
       clears the CVD floor, worst is person↔game at ΔE 12.2 under deuteranopia.
       Pre-existing and NOT fixed here: game↔type is ΔE 14.7 for normal vision,
       a shade under the 15 floor, because --seg-type is --air and --air is
       nearly achromatic. Raising its chroma repaints the density ridge, the
       note pins and every tag chip too — a house-colour decision, not a
       timeline one. */
    /* One vocabulary, one palette. `media` and `character` take the colours the
       snippet side already used for the same two ideas — violet for what it
       belongs to, blue for who is in it — so a tag looks the same wherever it
       appears. `type` inherits the orange that `game` vacated; the old house
       purple retired because it sat too close to violet to share a palette. */
    --seg-copyright: #c07fd8;   /* violet — what it belongs to */
    --seg-character: #5aa9e6;   /* blue   — who is in it */
    --seg-type:      #e0834f;   /* orange — collab, watchalong, karaoke, zatsudan */
    --seg-elements:  #6f6a7e;   /* grey, and grey means skippable: intro, break */
    --seg-unknown: transparent;

    /* The two snippet-only kinds. `media` and `character` are up there in the
       shared block — one vocabulary means one colour per subject, so a tag
       looks the same on a clip as it does on a strip. This ramp used to be a
       separate one, and the argument for holding it apart went away with the
       second table. */
    --seg-meta:      #e0c463;   /* yellow — what a clip IS */
    --seg-general:   #8f8a9e;   /* grey   — everything else about a clip */
    /* RED, because danbooru's artist category is red and this vocabulary is
       the same idea wearing this archive's words. Dark red, for two reasons
       that both have numbers.

       Every red collides with `--seg-type` under deuteranopia — red and
       orange are one hue to a deuteranope — so lightness is the only lever
       left, and the reds that clear this palette's floor of 15 are the dark
       ones. Measured the same way the block above was, ΔE2000 against every
       other kind under normal vision and all three dichromacies: danbooru's
       own #c00004 lands at 11.8 (type/deuteranopia), #b01010 at 15.4, and the
       rose this was first shipped as managed 3.6 against `meta` under
       tritanopia, which is why it is not that any more.

       And `--tl-not` below is #ff6b6b — red is already the colour of REMOVAL
       here, and an excluded tag is struck through in it on the same filter
       line these chips sit on. A bright red artist chip reads as "excluded"
       at a glance. This one is 20.1 from it. */
    --seg-artist:    #b01010;   /* red    — who made it */

    /* Aliases, kept so the forty rules written against the old names keep
       working. `--tl-meta` in particular is doing a second job as the generic
       warn/pending accent all over the snippet UI, which is why it is not
       simply deleted; untangling that is its own small change. */
    --tl-character: var(--seg-character);
    /* `media`, not `copyright`. The tag kind was renamed — the schema's own
       comment says media sits where copyright did — and this definition kept
       the old name while .cs-v.add went on asking for var(--tl-media). So the
       ADDED value in every changeset diff has been colourless since the
       rename: two dropped declarations, a colour and a border, both silent. */
    --tl-copyright: var(--seg-copyright);
    --tl-meta:      var(--seg-meta);
    --tl-general:   var(--seg-general);
    --tl-artist:    var(--seg-artist);
    /* Not a kind — the OPERATOR. An excluded tag is drawn in the colour of
       removal whatever kind it happens to be, because what matters about it on
       the filter line is that it is taking clips away. */
    --tl-not:       #ff6b6b;

    /* ---- glass ------------------------------------------------------------
       Named so it can spread one surface at a time — the sheet, the peek, the
       theater chrome can each adopt `.glass` without anyone re-deciding what
       the blur radius is.

       --glass is a TINT FLOOR, not a transparency. This sits over posters:
       bright, busy, arbitrary. 62% is roughly where --text-2 still holds up
       over the worst one in the collection, and thinning it is the change
       that looks great in a screenshot and fails in use. */
    --glass:        rgba(27, 25, 36, 0.62);
    --glass-2:      rgba(255, 255, 255, 0.045);   /* a card sitting ON glass */
    --glass-3:      rgba(255, 255, 255, 0.075);   /* its hover */
    --glass-line:   rgba(255, 255, 255, 0.10);
    /* The outline where a thing is not yet — see `.btn-add`. Six
       hand-written spellings of this collapsed into it; 0.18 is the
       middle of the three opacities that were in use. */
    --dash-line:    rgba(255, 255, 255, 0.18);
    /* The hairline under the name 24 rules were already asking for.
       `var(--border)` appears all over the ops panel, its queues, the model
       pickers and the log filters, and it was defined nowhere — so every one
       of those `border: 1px solid var(--border)` declarations was INVALID and
       dropped, and those cards and controls have never had a border. Nothing
       says so: an unresolvable var() takes its whole declaration with it, in
       silence, with no console warning and nothing in the inspector but a
       border that is not there.
       An alias rather than a second literal, so there is one hairline in the
       archive and retheming it stays one edit. */
    --border:       var(--glass-line);
    --glass-hi:     rgba(255, 255, 255, 0.13);    /* the lit top edge */
    --glass-sunk:   rgba(0, 0, 0, 0.24);          /* an input, pressed into it */
    --glass-blur:   blur(26px) saturate(165%);
    --glass-shadow: 0 28px 70px rgba(0,0,0,.52), 0 3px 10px rgba(0,0,0,.34);

    --th-chat-w: 340px;
    --th-sheet-h: calc(var(--vh) * 94);
    --th-head: 54px;
    --th-strip: 92px;
    --th-tl: 96px;
  }
  body.th-open { overflow: hidden; }

  .th-scrim { position: fixed; inset: 0; z-index: 40; background: rgba(10, 8, 14, 0.62);
    opacity: 0; pointer-events: none; transition: opacity 0.3s ease; }
  body.th-open .th-scrim { opacity: 1; pointer-events: auto; }

  .th { position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
    height: var(--th-sheet-h); display: flex; flex-direction: column;
    background: var(--panel); border-radius: 18px 18px 0 0;
    box-shadow: 0 -12px 60px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.06);
    transform: translateY(100%); transition: transform 0.34s cubic-bezier(0.22,1,0.36,1);
    padding: 0 20px 16px; }
  body.th-open .th { transform: none; }
  .th[hidden] { display: none; }
  .th-grab { width: 42px; height: 4px; border-radius: 999px; background: var(--surface-3);
    margin: 7px auto 0; flex: none; }

  /* ── header ── */
  .th-head { height: var(--th-head); flex: none; display: flex; align-items: center; gap: 12px; }
  .th-idx { font-family: var(--font-mono); font-size: 0.7rem; color: var(--accent);
    background: var(--accent-soft); padding: 3px 7px; border-radius: 6px; flex: none; }
  .th-title { font-family: var(--font-display); font-weight: 700; font-size: 1.05rem;
    letter-spacing: -0.02em; margin: 0; white-space: nowrap; overflow: hidden;
    text-overflow: ellipsis; min-width: 0; }
  .th-meta { display: flex; gap: 6px; flex: none; }
  .th-spacer { flex: 1 1 auto; }
  /* Tiles, the same size and shape as the header's platform links and the
     theater's own icon buttons, so a source reads as one thing across the
     page. align-items:center rather than stretch because the local tile hangs
     a caption below itself and must not make the row taller. */
  .th-src { display: flex; gap: 6px; flex: none; align-items: center; }
  .th-srcbtn { position: relative; width: 30px; height: 30px; border-radius: 8px;
    display: grid; place-items: center; flex: none; cursor: pointer;
    border: 1px solid rgba(255,255,255,0.1); background: transparent;
    color: var(--text-2); transition: color 0.15s ease, background 0.15s ease; }
  .th-srcbtn:hover:not(:disabled) { background: var(--surface-2); color: var(--text); }
  .th-srcbtn.plat--yt:hover:not(:disabled) { color: #ff4444; }
  .th-srcbtn.plat--tw:hover:not(:disabled) { color: #a970ff; }
  .th-srcbtn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent);
    background: var(--accent-soft); }
  .th-srcbtn:disabled { opacity: 0.35; cursor: not-allowed; }
  .th-srcbtn svg { display: block; }
  /* Which platform this file is a copy of — it decides the clock, so it is not
     decoration. Same shape as the header's LIVE tag, hung underneath instead of
     above, and absolutely positioned so it cannot push the 54px header taller. */
  .src-tag { position: absolute; bottom: -6px; left: 50%; transform: translateX(-50%);
    font-family: var(--font-mono); font-size: 0.44rem; font-weight: 500; letter-spacing: 0.08em;
    color: var(--text-3); background: var(--panel); padding: 0 3px; border-radius: 3px;
    line-height: 1.4; pointer-events: none; }
  /* Dead, or unreachable from this hostname. Top LEFT, because the default dot
     owns the right corner and a source can easily be both — the one you always
     open and the one this hostname cannot reach is exactly the pairing that
     shows up in development. */
  .src-x { position: absolute; top: -3px; left: -3px; font-size: 0.5rem; line-height: 1;
    color: var(--text-3); background: var(--panel); border-radius: 3px; padding: 0 1px; }
  /* Leaves the page. A mirror is a re-upload somewhere else — it opens in a
     tab rather than mounting, so the tile has to say so before it is pressed.
     Bottom right, which is free: .src-tag is bottom CENTRE and local-only, and
     the default dot owns the top right. */
  .src-out { position: absolute; bottom: -3px; right: -2px; font-size: 0.5rem;
    line-height: 1; color: var(--text-3); background: var(--panel);
    border-radius: 3px; padding: 0 1px; }
  /* The default: a dot, not a word. It marks one tile out of three or four in a
     54px header, where a label would be a fifth thing to read. Absent entirely
     when nothing has been chosen — an unset default is the automatic order, and
     drawing a dot on whatever that happens to resolve to would claim somebody
     decided it. */
  .th-srcbtn.is-default::after { content: ''; position: absolute; top: -2px; right: -2px;
    width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
    box-shadow: 0 0 0 2px var(--panel); }
  /* Set apart from the sources by a gap and quiet until you look for it — it is
     a tool, not another thing to play. */
  .th-wrench { margin-left: 6px; opacity: 0.55; }
  .th-wrench:hover, .th-wrench:focus-visible { opacity: 1; color: var(--accent); }
  .th-icon { background: var(--surface-2); border: 0; color: var(--text-2); width: 30px;
    height: 30px; border-radius: 8px; display: grid; place-items: center; flex: none;
    cursor: pointer; }
  .th-icon:hover { background: var(--surface-3); color: var(--text); }

  /* ── neighbour strip: archive order, older → newer, current centred ── */
  .th-strip { height: var(--th-strip); flex: none; display: flex; align-items: center; gap: 6px;
    border-top: 1px solid rgba(255,255,255,0.06); border-bottom: 1px solid rgba(255,255,255,0.06); }
  .th-strip-nav { background: transparent; border: 0; color: var(--text-3); width: 22px;
    height: 60px; display: grid; place-items: center; border-radius: 7px; flex: none;
    cursor: pointer; }
  .th-strip-nav:hover { background: var(--surface-2); color: var(--text); }
  /* position:relative is load-bearing, not cosmetic. Nothing between .th-nb and
     .th (position:fixed) was positioned, so offsetParent was the whole theater
     and offsetLeft was measured from ITS padding edge — while scrollLeft is
     expressed in this element's scroll-content coordinates. Everything laid out
     between the two origins (.th-edge + 6 + nav 22 + 6 + this padding 4) was a
     constant ~46px error in the centring below, invisible only because the
     browser clamps scrollLeft at both ends of a short rail. */
  .th-rail { position: relative; flex: 1 1 auto; display: flex; gap: 8px; overflow-x: auto; scroll-behavior: smooth;
    padding: 2px 4px; scrollbar-width: none;
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 26px, #000 calc(100% - 26px), transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0, #000 26px, #000 calc(100% - 26px), transparent 100%); }
  .th-rail::-webkit-scrollbar { display: none; }
  .th-nb-pad { flex: 0 0 calc(50% - 56px); pointer-events: none; }
  .th-nb { flex: none; width: 112px; cursor: pointer; border: 0; background: transparent;
    padding: 0; text-align: left; display: block; }
  .th-nb .nb-thumb { display: block; width: 112px; aspect-ratio: 16/9; border-radius: 7px; overflow: hidden;
    position: relative; box-shadow: 0 0 0 1px rgba(255,255,255,0.06);
    transition: box-shadow 0.15s ease, filter 0.15s ease; }
  .th-nb:hover .nb-thumb { filter: brightness(1.15); }
  .th-nb .nb-thumb img { position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; }
  .th-nb .nb-idx { position: absolute; left: 5px; bottom: 4px; font-family: var(--font-mono);
    font-size: 0.55rem; letter-spacing: 0.05em; color: var(--text-2);
    background: rgba(20,18,25,0.74); padding: 1px 4px; border-radius: 4px; z-index: 1; }
  .th-nb .nb-title { display: block; font-size: 0.63rem; color: var(--text-3); margin-top: 4px;
    line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .th-nb[aria-current="true"] .nb-thumb { box-shadow: 0 0 0 1.5px var(--accent); }
  .th-nb[aria-current="true"] .nb-title { color: var(--accent); font-weight: 600; }
  .th-edge { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: var(--text-3); flex: none; writing-mode: vertical-rl;
    opacity: 0.7; }

  /* ── body: stage + panel ── */
  .th-body { flex: 1 1 auto; min-height: 0; display: grid;
    grid-template-columns: minmax(0,1fr) var(--th-chat-w); gap: 16px; padding-top: 12px; }
  .th.panel-hidden .th-body { grid-template-columns: minmax(0,1fr) 0; }
  .th.panel-hidden .th-panel { opacity: 0; pointer-events: none; }
  /* The neighbour strip, folded. `display: none` rather than a height of zero,
     because the rail centres itself with offsetLeft on open and a collapsed
     but present row measures as garbage. The stage takes the space. */
  .th.rail-hidden .th-strip { display: none; }
  .th-left { min-width: 0; display: flex; flex-direction: column; gap: 12px; }

    /* Height-bound, never width-bound: the stage takes whatever the chrome
     leaves, and the 16:9 ratio derives the width from it. Widescreen monitors
     get side gutters, not a clipped player. */
  .th-stage { flex: 1 1 auto; min-height: 0; display: flex; justify-content: center; }
  .th-player { aspect-ratio: 16/9; height: 100%; max-width: 100%; margin: 0 auto; border-radius: 12px; background: #0c0a10; position: relative;
    overflow: hidden; box-shadow: 0 0 0 1px rgba(255,255,255,0.05); }
  .th-player iframe, .th-player video { position: absolute; inset: 0; width: 100%; height: 100%;
    border: 0; }
  .th-player .glyph svg { width: 46px; height: 46px; }

  /* The tombstone. A stream with nothing playable is not an error state — the
     schema calls it the most valuable row in the archive — so it gets a real
     face rather than an empty box. */
  .th-gone { position: absolute; inset: 0; display: grid; place-content: center;
    justify-items: center; gap: 8px; text-align: center; padding: 20px; }
  .th-gone svg { color: var(--text-3); opacity: 0.5; }
  .th-gone b { font-family: var(--font-display); font-size: 1rem; font-weight: 700; }
  .th-gone span { font-size: 0.78rem; color: var(--text-3); max-width: 42ch; }

  /* ── timeline: coverage, segment bar, note pins, one x-axis ── */
  .th-tl { flex: none; height: var(--th-tl); display: flex; flex-direction: column; gap: 6px; }
  .th-tl-head { height: 18px; display: flex; align-items: center; gap: 12px; }
  .th-now { display: flex; align-items: center; gap: 7px; font-size: 0.76rem; font-weight: 600;
    min-width: 0; }
  .th-now i { width: 8px; height: 8px; border-radius: 2px; background: var(--seg-elements);
    flex: none; }
  .th-now b { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* Nikke › No Caller ID. A chevron, not a second line: the two facts are one
     answer to "what is on screen", and stacking them would make them two. */
  .th-now b .th-now-in { color: var(--text-3); margin: 0 5px; font-weight: 400; }
  .th-now span { color: var(--text-3); font-weight: 400; font-family: var(--font-mono);
    font-size: 0.64rem; white-space: nowrap; }
  .th-legend { margin-left: auto; display: flex; gap: 12px; flex: none; }
  .th-legend b { display: flex; align-items: center; gap: 5px; font-family: var(--font-mono);
    font-size: 0.52rem; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-3); font-weight: 500; }
  .th-legend i { width: 7px; height: 7px; border-radius: 2px; }
  .th-legend i.hatch { background-color: rgba(255,255,255,0.06);
    background-image: repeating-linear-gradient(45deg,
      rgba(255,255,255,0.30) 0 1.5px, rgba(255,255,255,0) 1.5px 3px); }

  .th-tl-body { position: relative; flex: 1 1 auto; min-height: 0; cursor: pointer;
    display: flex; flex-direction: column; justify-content: flex-end; }

  /* ── how much was being said ────────────────────────────────────────────
     Above the pins rather than across the track. "Overlaid on the timeline"
     is what it was, and what that cost was legibility: the blocks are the
     thing being read, and a curve drawn over them is drawn on top of the
     answer. Same axis, its own 26px.

     `pointer-events: none`, and not optionally: .th-tl-body owns the
     hairline, the bubble and click-to-seek, and a child that swallowed a
     pointer would put a dead stripe across all three. What is worth reading
     off the curve — the rate under the cursor — goes into the bubble
     instead, which is already following the pointer. */
  .th-rate { height: 26px; position: relative; flex: none; margin-bottom: 3px;
    pointer-events: none; }
  .th-rate svg { display: block; width: 100%; height: 100%; }
  /* The viewBox is one unit per bucket and preserveAspectRatio is off, so the
     buckets stretch to the row and bucket i lands exactly where thPct() would
     put it — no pixel arithmetic, and it stays right through a resize. That
     is the whole reason this is an SVG and not a row of divs.
     non-scaling-stroke because that same stretch would otherwise take the
     stroke with it: 260 × 100 drawn into 900 × 26 makes a line four times
     wider than it is tall. */
  .th-rate path { vector-effect: non-scaling-stroke; }
  .th-rate .all { fill: rgba(255,255,255,0.075); stroke: rgba(255,255,255,0.26);
    stroke-width: 1; }
  .th-rate .hit { fill: color-mix(in srgb, var(--accent) 24%, transparent);
    stroke: var(--accent); stroke-width: 1.4; }
  /* Searching drops the total to a ghost. Two ridges at two different scales
     drawn with the same weight is a chart with two unlabelled y-axes: the
     foreground is whichever one the key is naming, and only one at a time. */
  /* Faint, but still readable — a ghost nobody can see is the same as not
     drawing it, and the point of keeping it is that you can tell whether the
     thing you searched for happened where the chat was busy or somewhere it
     was not. */
  .th-rate.q .all { fill: rgba(255,255,255,0.045); stroke: rgba(255,255,255,0.17); }
  .th-rate-key { display: flex; align-items: center; gap: 7px; flex: none;
    margin-left: 14px; font-family: var(--font-mono); font-size: 0.52rem;
    letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-3);
    min-width: 0; }
  .th-rate-key i { width: 7px; height: 7px; border-radius: 2px; flex: none;
    background: rgba(255,255,255,0.26); }
  .th-rate-key i.hit { background: var(--accent); }
  /* `text-transform: none`, against the uppercase the row sets: this is the
     query as it was typed, and an emote's name is not case-insensitive
     decoration — `theoKEK` uppercased to THEOKEK is the wrong word. */
  .th-rate-key b { font-weight: 600; color: var(--text-2); overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; max-width: 16ch;
    text-transform: none; letter-spacing: 0.02em; }
  .th-rate-key s { text-decoration: none; color: var(--text-3); opacity: 0.7; }

  .th-pins { height: 12px; position: relative; flex: none; }
  .th-track { height: 18px; position: relative; border-radius: 5px; overflow: hidden;
    flex: none; background: rgba(255,255,255,0.05); }
  .th-seg { position: absolute; top: 0; bottom: 0; display: flex; align-items: center;
    padding: 0 6px; overflow: hidden; transition: filter 0.15s ease; }
  /* A 2px surface gap between fills, so adjacent blocks read as two things. */
  .th-seg + .th-seg { box-shadow: -2px 0 0 var(--panel); }
  .th-seg span { font-family: var(--font-mono); font-size: 0.53rem; letter-spacing: 0.06em;
    text-transform: uppercase; white-space: nowrap; color: rgba(20,18,25,0.78); font-weight: 500; }
  /* Unlabelled time. Hatch, not a swatch — nobody said this was dead air. */
  .th-seg.unknown { background-image: repeating-linear-gradient(45deg,
      rgba(255,255,255,0.055) 0 5px, rgba(255,255,255,0) 5px 10px); }
  .th-seg.unknown span { color: var(--text-3); }
  .th-tl-body:hover .th-seg { filter: brightness(1.12); }

  /* ── the second lane ──────────────────────────────────────────────────────
     What was happening INSIDE what the row above names. Shorter, transparent
     where nothing is claimed, and tinted with its container's own hue rather
     than a colour of its own — so the ramp stays the four that were validated
     against #1b1924, and "part of that" is carried by size and tint instead of
     by a fifth swatch, a bracket or a leader line.
     `--u` is the containing block's colour, written per element by
     thRenderTimeline from the projection's `under.kind`. The flat
     surface-2 fill is declared first as the fallback for anything without
     color-mix; the tint is the enhancement. */
  .th-sub { height: 11px; margin-top: 3px; background: none; overflow: visible; }
  .th-sub .th-seg {
    border-radius: 3px; padding: 0 5px;
    background: var(--surface-2);
    background: color-mix(in srgb, var(--u, var(--surface-3)) 30%, var(--panel));
    border-top: 2px solid var(--u, var(--surface-3)); }
  .th-sub .th-seg + .th-seg { box-shadow: none; }
  .th-sub .th-seg span { color: var(--text); text-transform: none; letter-spacing: 0.04em; }

  /* Per-source coverage. A Twitch VOD that began two minutes before the
     YouTube one starts at a negative offset, and the toggle needs to say so
     rather than pretending both cover the same span. */
  .th-cov { height: 5px; position: relative; flex: none; margin-top: 3px; }
  .th-cov i { position: absolute; height: 3px; top: 1px; border-radius: 2px; opacity: 0.85; }
  .th-cov i.yt { background: #ff4444; }
  .th-cov i.tw { background: #a970ff; }
  .th-cov i.local { background: var(--air); }

  .th-axis { height: 13px; position: relative; flex: none; margin-top: 2px; }
  .th-tick { position: absolute; top: 0; font-family: var(--font-mono); font-size: 0.54rem;
    color: var(--text-3); transform: translateX(-50%); font-variant-numeric: tabular-nums; }
  .th-tick.minor::before { content: ''; position: absolute; left: 50%; top: -5px; width: 1px;
    height: 3px; background: rgba(255,255,255,0.12); }

  /* Pins: the brand mark, reused — a rotated square on a hairline stem. */
  .th-pin { position: absolute; bottom: 0; transform: translateX(-50%); width: 14px; height: 12px;
    background: transparent; border: 0; padding: 0; cursor: pointer; }
  .th-pin::before { content: ''; position: absolute; left: 50%; top: 1px; width: 7px; height: 7px;
    margin-left: -3.5px; background: var(--air); border-radius: 1.5px; transform: rotate(45deg);
    transition: background 0.15s ease, transform 0.15s ease; }
  .th-pin::after { content: ''; position: absolute; left: 50%; bottom: 0; width: 1px; height: 4px;
    background: rgba(183,151,189,0.5); }
  .th-pin:hover::before, .th-pin.on::before { background: var(--accent);
    transform: rotate(45deg) scale(1.25); }
  /* A ±2min note drawn as a hairline is the timeline lying. The band is the
     error bar; the diamond stays put so the pin is still clickable. */
  .th-pin .band { position: absolute; bottom: 1px; height: 3px; border-radius: 2px;
    background: rgba(183,151,189,0.28); transform: translateX(-50%); left: 50%; }
  .th-pin.fuzzy::before { opacity: 0.75; }
  /* A NOTE WITH A LENGTH, drawn as the length. Not the `.band` above it: that
     is an error bar, symmetric around an uncertain point, and this is a span
     somebody wrote down. Under the diamonds and behind them, so a pin sitting
     at the start of its own bar is still the thing you click. */
  .th-span { position: absolute; bottom: 1px; height: 3px; border-radius: 2px;
    background: color-mix(in srgb, var(--air) 42%, transparent);
    pointer-events: none; z-index: 0; }

  .th-playhead { position: absolute; top: 0; bottom: 15px; width: 2px; margin-left: -1px;
    background: var(--accent); border-radius: 2px; pointer-events: none; z-index: 3; }
  .th-playhead::after { content: ''; position: absolute; left: 50%; top: -4px; width: 11px;
    height: 11px; margin-left: -5.5px; border-radius: 50%; background: var(--accent);
    box-shadow: 0 0 0 2px var(--panel); }
  .th-hair { position: absolute; top: 0; bottom: 15px; width: 1px;
    background: rgba(255,255,255,0.35); pointer-events: none; z-index: 2; opacity: 0; }
  .th-tl-body:hover .th-hair { opacity: 1; }
  /* When the hairline has landed on a boundary the block will actually take,
     it stops being a guide and becomes a promise. Say so. */
  .th-hair.snapped { background: var(--accent); width: 2px; margin-left: -0.5px; }

  /* ── drawing a chapter ──────────────────────────────────────────────────
     Draw mode is explicit, because the same click already means "seek" and a
     timeline that sometimes seeks and sometimes starts an edit is a timeline
     you stop trusting with the mouse. */
  .th-draw-btn { flex: none; margin-left: 12px; border-radius: 999px;
    padding: 3px 10px; font-size: 0.54rem;
    letter-spacing: 0.07em; text-transform: uppercase; }
  .th-draw-btn[aria-pressed="true"] { border-style: solid; border-color: var(--accent);
    background: var(--accent); color: var(--on-accent); }
  .th-tl-body.drawing { cursor: crosshair; }
  /* Existing blocks stop inviting a click while drawing — clicking one opens
     it for editing instead of seeking, and the cursor should say which. */
  .th-tl-body.drawing .th-seg { cursor: crosshair; }
  .th-ghost { position: absolute; z-index: 4; pointer-events: none; border-radius: 4px;
    background: rgba(224,131,79,0.28); box-shadow: inset 0 0 0 1px var(--accent);
    display: flex; align-items: center; justify-content: center; }
  .th-ghost b { font-family: var(--font-mono); font-size: 0.55rem; color: var(--text);
    font-weight: 500; white-space: nowrap; }
  /* An author `display` outranks the UA sheet's [hidden] rule, so anything with
     one has to opt back in or the hidden attribute quietly does nothing — which
     is why every other display-carrying element in this file has the same
     line. Without it the ghost stays painted over the block it just became. */
  .th-ghost[hidden] { display: none; }
  .th-live { font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: #ff6b6b; display: inline-flex; align-items: center;
    gap: 5px; }
  .th-live::before { content: ''; width: 6px; height: 6px; border-radius: 50%;
    background: currentColor; }
  .th-live[hidden] { display: none; }
  /* A statement, and a way to act on it. Blocks that fall past the end of the
     stream are not drawn — there is nowhere on the strip to draw them — so
     this is the only place they exist on screen, and pressing it opens the
     first one where its numbers can be argued with. */
  .th-past { border: 0; border-radius: 7px; padding: 3px 8px; cursor: pointer;
    font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.05em;
    background: rgba(255,179,179,0.14); color: #ffb3b3; }
  .th-past:hover { background: rgba(255,179,179,0.24); }
  .th-past[hidden] { display: none; }
  /* The live chapter list. Above the notes rather than in them: these are not
     notes, they are the strip in a form that holds still. */
  .mk-sec { margin: 0 0 10px; padding: 8px 8px 7px; border-radius: 10px;
    background: var(--surface-2); }
  .mk-head { font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.09em;
    text-transform: uppercase; color: var(--text-3); margin-bottom: 6px; }
  .mk-row { display: flex; align-items: baseline; gap: 7px; padding: 3px 4px;
    border-radius: 6px; cursor: pointer; }
  .mk-row:hover { background: var(--surface-3); }
  .mk-row.sub { padding-left: 16px; }
  .mk-g { font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-3); }
  .mk-n { flex: 1 1 auto; min-width: 0; font-size: 0.76rem; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .mk-t { font-family: var(--font-mono); font-size: 0.58rem; color: var(--text-3); }
  /* Still running. The dot is the same one the live chip uses, because it is
     the same claim: this has started and has not stopped. */
  .mk-row.open .mk-t::after { content: ''; display: inline-block; margin-left: 5px;
    width: 5px; height: 5px; border-radius: 50%; background: #ff6b6b;
    vertical-align: middle; }
  .mk-none { font-size: 0.74rem; color: var(--text-3); padding: 2px 4px 4px; }
  /* The Chapters tab. Deliberately close to .mk-row, because it is the same
     claim in the same shape — the difference is that this one also lists the
     stretches nobody has named, and those are the rows that get a button. */
  .cp-row { display: flex; align-items: baseline; gap: 8px; padding: 4px 6px;
    border-radius: 6px; cursor: pointer; }
  .cp-row:hover { background: var(--surface-2); }
  .cp-row.on { background: var(--surface-3); }
  .cp-row.sub { padding-left: 22px; }
  .cp-dot { flex: none; width: 7px; height: 7px; border-radius: 2px;
    background: var(--surface-3); align-self: center; }
  .cp-row.sub .cp-dot { width: 5px; height: 5px; border-radius: 50%; }
  .cp-n { flex: 1 1 auto; min-width: 0; font-size: 0.78rem; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .cp-t { flex: none; font-family: var(--font-mono); font-size: 0.58rem;
    color: var(--text-3); }
  /* A gap is a statement about the absence of a record, so it is drawn as an
     absence: dashed, unfilled, the name in the muted voice. It is still a row
     you can click — that is the entire point of the tab. */
  .cp-row.gap .cp-dot { background: transparent; border: 1px dashed var(--text-3); }
  .cp-row.gap .cp-n { color: var(--text-3); font-style: italic; }
  .cp-add { flex: none; background: var(--surface-2); border: 0; color: var(--text-2);
    font-family: var(--font-mono); font-size: 0.6rem; line-height: 1;
    padding: 3px 6px; border-radius: 5px; cursor: pointer; opacity: 0; }
  .cp-row:hover .cp-add, .cp-add:focus-visible { opacity: 1; }
  .cp-add:hover { background: var(--surface-3); color: var(--text); }
  /* Always visible on a gap. Everywhere else the button is an offer; here it
     is the instruction, and hiding it until hover would hide the one thing
     this tab exists to make findable. */
  .cp-row.gap .cp-add { opacity: 1; }
  .cp-row.open .cp-t::after { content: ''; display: inline-block; margin-left: 5px;
    width: 5px; height: 5px; border-radius: 50%; background: #ff6b6b;
    vertical-align: middle; }
  .mk-help { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--glass-line);
    font-size: 0.66rem; line-height: 1.55; color: var(--text-3); }
  .mk-help b { font-family: var(--font-mono); color: var(--text-2); font-weight: 600; }
  .mk-help code { font-family: var(--font-mono); font-size: 0.62rem;
    background: var(--glass-sunk); padding: 1px 4px; border-radius: 4px; }
  /* Three rows, before three rows are written. Monospace on purpose: the
     numbers line up, and lining up is the whole reason to show it. */
  .ch-split { display: grid; grid-template-columns: auto 1fr; gap: 3px 10px;
    align-items: baseline; margin: 2px 0 9px; padding: 8px 9px; border-radius: 8px;
    background: var(--glass-sunk); }
  .ch-split b { grid-column: 1 / -1; font-size: 0.7rem; font-weight: 600;
    color: var(--text-2); margin-bottom: 3px; }
  .ch-split i { font-style: normal; font-size: 0.68rem; color: var(--text-2);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 150px; }
  .ch-split i.new { color: var(--accent); }
  .ch-split span { font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-3); }
  .ch-split[hidden] { display: none; }
  /* Shown only while a block is open for editing. The ghost is
     pointer-events:none so that a half-drawn block never eats a click meant
     for the strip; these have to opt back in. */
  .th-grip { position: absolute; top: 0; bottom: 0; width: 11px; display: none;
    cursor: ew-resize; pointer-events: auto; }
  .th-grip::after { content: ''; position: absolute; top: 50%; left: 50%;
    transform: translate(-50%, -50%); width: 3px; height: 62%; min-height: 9px;
    border-radius: 2px; background: var(--accent);
    box-shadow: 0 0 0 1px rgba(0,0,0,0.35); }
  .th-grip[data-grip="in"] { left: -5px; }
  .th-grip[data-grip="out"] { right: -5px; }
  .th-ghost.editing .th-grip { display: block; }
  .th-mark { position: absolute; top: 0; bottom: 15px; width: 2px; margin-left: -1px;
    background: var(--accent); z-index: 5; pointer-events: none; }
  .th-mark::before { content: ''; position: absolute; left: 50%; top: -3px; width: 9px;
    height: 9px; margin-left: -4.5px; border-radius: 2px; background: var(--accent);
    transform: rotate(45deg); }

  .th-bubble, .th-pop { position: absolute; bottom: calc(100% + 10px);
    transform: translateX(-50%); z-index: 6; pointer-events: none; opacity: 0;
    transition: opacity 0.12s ease; }
  .th-bubble.on, .th-pop.on { opacity: 1; }
  .th-bubble { font-family: var(--font-mono); font-size: 0.64rem; color: var(--text);
    background: var(--surface-3); padding: 3px 8px; border-radius: 6px; white-space: nowrap; }
  .th-pop { width: 244px; background: var(--panel); border-radius: 10px; padding: 10px 11px;
    box-shadow: 0 0 0 1px rgba(255,255,255,0.09), 0 18px 44px rgba(0,0,0,0.6); }
  .th-pop .pop-t { display: flex; align-items: center; gap: 7px; margin-bottom: 5px; }
  .th-pop .pop-tag { font-family: var(--font-mono); font-size: 0.6rem; color: var(--air); }
  .th-pop .pop-ts { font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-3);
    margin-left: auto; }
  .th-pop .pop-x { font-size: 0.78rem; line-height: 1.45; color: var(--text); }
  .th-pop .pop-go { font-family: var(--font-mono); font-size: 0.54rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--text-3); margin-top: 7px; display: block; }

  /* No duration means no domain, so there is nothing to scale to. Say that
     rather than drawing a strip against a number nobody has. */
  .th-nodomain { flex: 1 1 auto; display: flex; align-items: center; gap: 10px;
    font-size: 0.74rem; color: var(--text-3); }
  .th-nodomain .pill { flex: none; }
  .th-nodomain[hidden] { display: none; }
  /* Hidden, not replaced. Writing this message into th-tl-body.innerHTML
     detached #th-track, #th-pins and the rest — and TH holds those references
     from page load, so every stream opened afterwards rendered its strip into
     a node that was no longer in the document. */
  .th-tl-body.nodomain > :not(.th-nodomain) { display: none; }
  .th-tl-body.nodomain { cursor: default; }

  /* ── side panel ── */
  .th-panel { background: var(--surface); border-radius: var(--radius); display: flex;
    flex-direction: column; min-height: 0; overflow: hidden; transition: opacity 0.2s ease;
    box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05); }
  .th-panel-head { flex: none; display: flex; align-items: center; gap: 8px; padding: 10px;
    border-bottom: 1px solid rgba(255,255,255,0.06); }
  .th-seg-ctl { display: flex; gap: 2px; background: var(--surface-2); padding: 2px;
    border-radius: 9px; }
  .th-seg-ctl button { border: 0; background: transparent; color: var(--text-2);
    font-weight: 600; font-size: 0.78rem; padding: 5px 13px; border-radius: 7px; cursor: pointer; }
  .th-seg-ctl button[aria-selected="true"] { background: var(--surface-3); color: var(--text); }
  .th-count { font-family: var(--font-mono); font-size: 0.58rem; color: var(--text-3);
    margin-left: auto; }
  /* Smaller than .th-icon: the header's buttons are the theater's chrome and
     this one sits inside a panel, next to a 28px tab control. */
  .th-pico { flex: none; width: 26px; height: 26px; border: 0; border-radius: 7px;
    background: var(--surface-2); color: var(--text-2); display: grid;
    place-items: center; cursor: pointer; }
  .th-pico svg { width: 13px; height: 13px; }
  .th-pico:hover { background: var(--surface-3); color: var(--text); }
  .th-pico:disabled { opacity: 0.5; cursor: default; }
  .th-pico[hidden] { display: none; }

  .th-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 8px 4px 8px 10px; }
  .th-list::-webkit-scrollbar { width: 8px; }
  .th-list::-webkit-scrollbar-thumb { background: var(--surface-3); border-radius: 4px; }
  .th-list[hidden] { display: none; }

  .nt { display: flex; align-items: baseline; gap: 7px; padding: 5px 6px; border-radius: 7px;
    font-size: 0.78rem; cursor: pointer; }
  .nt:hover { background: var(--surface-2); }
  .nt.on { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent); }
  .nt-tag { font-family: var(--font-mono); font-size: 0.62rem; color: var(--air); flex: none; }
  .nt-x { flex: 1 1 auto; min-width: 0; color: var(--text-2); }
  .nt-x.done { color: var(--text-3); }
  .nt-x i { opacity: 0.5; }
  .nt.on .nt-x { color: var(--text); }
  /* Which clock the number came from, as one dot. Four answers on a
     dual-platform stream and a fifth — nobody wrote it down — which is what
     every imported note still says. Hollow means exactly that. */
  .nt-group { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: var(--text-3); padding: 10px 6px 2px; }
  /* Sticky, because a grouped list is only readable if you can still see which
     group you are in after scrolling into it. */
  .nt-group { position: sticky; top: 0; background: var(--surface); z-index: 1; }
  .nt-new { padding: 2px 2px 8px; position: sticky; top: 0; z-index: 2;
    background: var(--surface); }
  /* The frozen moment, shown because a frozen number is otherwise invisible
     state — you cannot tell a stale @ from a fresh one without it. */
  .arm-hint { display: block; margin-top: 4px; font-family: var(--font-mono);
    font-size: 0.56rem; letter-spacing: 0.05em; color: var(--accent); opacity: 0.85; }
  .arm-hint[hidden] { display: none; }
  /* The wall-clock conversion. Deliberately NOT the accent: the arm hint says
     "a number is ready", this says "your number means something else", and two
     different claims in the same colour read as one. */
  .wall-hint { display: block; margin-top: 4px; font-family: var(--font-mono);
    font-size: 0.56rem; letter-spacing: 0.05em; color: var(--text-2); }
  .wall-hint[hidden] { display: none; }
  .wall-hint.past { color: #ffb3b3; }   /* same red the strip flags a past-the-end note in */
  /* The chip's swatch for it. Grey-blue, distinct from the platform clocks and
     from the axis, because a time of day is not a position in anything. */
  .ob-clk.wall, .nte-dot.wall { background: #7f9ec4; }
  /* Set to a clock this surface cannot resolve. Loud on purpose: whatever you
     type next cannot be converted, and the field will refuse it. */
  .nt-cp.is-gone { border-color: #ffb3b3; color: #ffb3b3; }

  /* ── the capture bar ────────────────────────────────────────────────────
     Floating and bottom-centred, because it is used while watching something
     that is not this page. It only ever appears when neither the theater nor
     the notes column is on screen — when one of them is, the key sends you to
     the field that is already there — so it never has to share the bottom of
     the viewport with the theater's timeline.

     z-index 89: over the sheet (50) and the drag hint (88), under the toasts
     (90), which is the one thing that must never end up behind it — the
     refusals this field can raise arrive as toasts. */
  .cap { position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%);
    z-index: 89; width: min(620px, calc(calc(var(--vw) * 100) - 28px));
    background: var(--panel); border-radius: 12px; padding: 9px 11px 8px;
    box-shadow: 0 0 0 1px rgba(255,255,255,0.1), 0 20px 50px rgba(0,0,0,0.62); }
  .cap[hidden] { display: none; }
  .cap-in { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .cap-lab { flex: none; font-family: var(--font-mono); font-size: 0.56rem;
    letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }
  .cap-f { flex: 1 1 220px; min-width: 0; resize: none; overflow: hidden;
    background: var(--surface-2); color: var(--text); font-family: inherit;
    font-size: 0.78rem; border: 1px solid var(--surface-3); border-radius: 8px;
    padding: 7px 9px; }
  .cap-f::placeholder { color: var(--text-3); }
  .cap-f:focus { outline: none; border-color: var(--accent); }
  .cap-f:disabled { opacity: 0.5; }
  .cap-x { flex: none; background: transparent; border: 0; border-radius: 5px;
    padding: 2px 5px; color: var(--text-3); cursor: pointer; font-size: 0.8rem; }
  .cap-x:hover { color: var(--text); background: var(--surface-2); }
  .cap .arm-hint, .cap .wall-hint { flex: 1 0 100%; }
  /* Which stream this is about to be written to. Said out loud because the bar
     is reachable from anywhere, including from pages that name no stream at
     all, and "a note went somewhere" is not a thing you can check later. */
  .cap-where { margin-top: 6px; font-size: 0.63rem; color: var(--text-3);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .cap-where b { color: var(--text-2); font-weight: 600; }
  .cap-where i { font-style: normal; color: var(--accent); }
  .nt-new textarea { width: 100%; box-sizing: border-box; background: transparent;
    border: 1px dashed rgba(255,255,255,0.16); border-radius: 8px; color: var(--text);
    padding: 7px 9px; font-family: inherit; font-size: 0.76rem; }
  .nt-new textarea::placeholder { color: var(--text-3); }
  .nt-new textarea:focus { outline: none; border-style: solid; border-color: var(--accent);
    background: var(--surface-2); }
  .nt-new textarea:disabled { opacity: 0.5; }
  /* `wrap`, so a ranged note's second line sits under the first instead of
     widening a column that is already competing with the note's own text. The
     dot stays on the first line with the start it belongs to.

     RIGHT-ALIGNED, EVERY LINE, and that is not cosmetic. The box is `flex:
     none`, so it is as wide as its WIDEST line — which on a ranged note is the
     second one, dash and all. Left-aligned inside it, the start timestamp then
     stopped short of the right edge by however much wider the end's line was,
     and a rail of notes had its times stepping in and out by a few pixels per
     row depending on what each one happened to say. Aligning to the end
     instead makes the edge the same number for every row in the panel.

     `align-items: center` CENTRES WITHIN EACH FLEX LINE, not across both —
     that is what `wrap` buys, and it is why the clock dot can be centred on
     the start it belongs to without drifting toward the end on a two-line row.
     `flex-start` put a 6px dot at the top of a 14px line box, which read as
     the dot sitting above its own timestamp.

     THE PADDING IS A RESERVED GUTTER for the ellipsis below, held open on
     every row whether or not one is drawn. The ellipsis cannot sit in the flow
     or it becomes the rightmost thing on its line and pushes that line's
     timestamp out of the column — which is the same bug as the one above, from
     the other side. One number, here, sets where every line ends. */
  .nt-when { display: flex; flex-wrap: wrap; align-items: center;
    justify-content: flex-end; gap: 0 4px; flex: none; align-self: flex-start;
    position: relative; padding: 1px 10px 0 0; }
  /* THE SECOND LINE. `width: 100%` is what breaks it — a flex item that fills
     the line forces the wrap without a <br>, which would put a line break
     inside a value that is sometimes one line and sometimes two. Right-aligned
     like the line above it, so the two timestamps end on the same pixel and
     the dash hangs off the left the way the dot does. */
  .nt-span { width: 100%; display: flex; align-items: center;
    justify-content: flex-end; gap: 3px; }
  .nt-dash { font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-3);
    opacity: 0.6; }
  /* "and more after this". Dimmer than the dash it sits beside, and `help` so
     the pointer says there is something to read — the rest of the stamp is in
     the title and nowhere else on screen.
     OUT OF THE FLOW, in the gutter `.nt-when` holds open for it: in the flow
     it is the rightmost thing on its line, and the timestamp beside it then
     ends a character short of every other row's. `bottom`, not `top`, so on a
     ranged note it lands beside the END — the last thing said — rather than
     looking like a property of the start. */
  .nt-more { position: absolute; right: 0; bottom: 0;
    font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-3);
    opacity: 0.5; cursor: help; letter-spacing: 0.5px; }
  .nt-more:hover { opacity: 0.9; }
  .nt-clk { width: 6px; height: 6px; border-radius: 50%; flex: none; }
  .nt-clk.yt { background: #ff4444; }
  .nt-clk.tw { background: #a970ff; }
  .nt-clk.local { background: var(--air); }
  .nt-clk.axis { background: var(--accent); }
  .nt-clk.unset { box-shadow: inset 0 0 0 1px var(--text-3); }
  .nt-edit { flex: none; border: 0; background: transparent; color: var(--text-3);
    cursor: pointer; padding: 0 2px; font-size: 0.72rem; line-height: 1; opacity: 0;
    transition: opacity 0.12s ease; }
  .nt:hover .nt-edit, .nt-edit:focus-visible { opacity: 1; }
  .nt-edit:hover { color: var(--accent); }
  .nt-ts { font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-3); flex: none;
    font-variant-numeric: tabular-nums; }
  /* An unknown-frame timestamp is a recollection, not a measurement. */
  .nt-ts.fuzzy::after { content: '~'; opacity: 0.65; margin-left: 1px; }
  .nt-ts.none { opacity: 0.5; }
  /* Past the end of the stream: the timestamp is wrong, or the duration is. */
  .nt-ts.past { color: #ffb3b3; }
  .nt-ts.past::after { content: '!'; margin-left: 2px; }
  /* Outside the SOURCE on screen, which is not outside the stream — a note
     half a minute before a YouTube recording that started late is a correct
     note about a real moment, so it is dimmed rather than flagged red. The
     sign on the number already says which side it fell off. */
  .nt-ts.offsrc { opacity: 0.6; }

  .th-empty { padding: 22px 14px; text-align: center; color: var(--text-3); font-size: 0.78rem;
    line-height: 1.6; }
  .th-empty b { display: block; color: var(--text-2); font-weight: 600; margin-bottom: 4px; }

  /* ── chat ──────────────────────────────────────────────────────────────
     A 2px rule down the left says where a message came from, and nothing else
     does. The tinted-background version of this was tried on paper and reads
     as noise the moment a dual-platform stream interleaves: five hundred rows
     of alternating wash, and the thing you are actually reading is the words.
     A rule at the edge is legible at a glance and invisible while reading. */
  /* By id: the element is `<div class="th-list" id="th-chat">`, and a class
     selector here would have matched nothing while every rule below still
     looked correct. */
  #th-chat { --ch-yt: #ff4d4d; --ch-tw: #a970ff;
    --ch-mem: #46d17a; --ch-mod: #6c9cff; --ch-owner: #ff6161; --ch-vip: #e46bd6;
    --ch-del: #ff7a7a; }
  .ch { border-left: 2px solid transparent; padding: 2px 4px 2px 7px; margin-bottom: 1px;
    font-size: 0.76rem; line-height: 1.5; overflow-wrap: anywhere; cursor: pointer;
    border-radius: 0 4px 4px 0; }
  .ch:hover { background: rgba(255,255,255,0.035); }
  .ch.yt { border-left-color: var(--ch-yt); }
  .ch.tw { border-left-color: var(--ch-tw); }
  /* The name's colour is the only thing on a row that says who is talking, so
     it carries status on its own — a badge picture that never arrived, or a
     platform that has no picture for it, must not take the answer with it. */
  .ch-n { font-weight: 600; color: var(--text-2); }
  /* A name is a control now. Underlined only on hover, because thirty thousand
     permanently underlined names is a log you cannot read. */
  .ch-n[data-chname] { cursor: pointer; }
  .ch-n[data-chname]:hover { text-decoration: underline; text-underline-offset: 2px; }
  .ch-n.mem   { color: var(--ch-mem); }
  .ch-n.mod   { color: var(--ch-mod); }
  .ch-n.owner { color: var(--ch-owner); }
  .ch-n.vip   { color: var(--ch-vip); }
  .ch-c { color: var(--text-3); margin-right: 3px; }
  .ch-t { color: var(--text); }
  /* Struck through AND red. Struck alone reads as a formatting flourish at
     this size; red alone reads as emphasis. Together they read as removed,
     which is what happened. */
  .ch.del .ch-t { color: var(--ch-del); text-decoration: line-through; }
  .ch-b { height: 13px; width: auto; vertical-align: -2px; margin-right: 3px; }
  .ch-e { height: 24px; width: auto; vertical-align: -7px; margin: -4px 1px; }
  /* Larger than an emote: a gift is the whole content of its row rather than
     one word inside a sentence, and several of them are animations that read
     as a smudge at 24px. */
  .ch-g { height: 32px; width: auto; vertical-align: -10px; margin: -5px 2px; }
  /* A picture nobody fetched leaves the badge out and the emote as its own
     name — `:tenmaWow:` is still readable, a broken-image glyph is not. */
  .ch-b.gone { display: none; }
  .ch-amt { display: inline-block; background: var(--surface-3); color: var(--text);
    border-radius: 4px; padding: 0 4px; margin-right: 4px; font-size: 0.68rem;
    font-weight: 600; }
  .ch-sys { color: var(--text-3); font-style: italic; }

  /* A YouTube superchat or membership is a card, the way it was on the day.
     No left rule on these: the card's own colour already says where it came
     from, and a red 2px stripe beside a red card is a smudge. */
  .ch.card { border-left-color: transparent; padding-left: 4px; padding-right: 4px; }
  .ch-card { border-radius: 8px; overflow: hidden; }
  .ch-card-h { background: var(--hb); color: var(--ht); padding: 6px 8px;
    display: flex; align-items: flex-start; gap: 7px; }
  /* A plain black square where the picture goes. Nothing is stored about a
     chatter's avatar and nothing should be — that is one image per person
     across the archive — so this is a hole of a known size and colour, which
     keys out cleanly in an edit and does not look like a broken image in the
     panel. */
  /* A picture if one has been put there, and a black hole if not. The hole
     is the background, so a missing file needs no fallback logic anywhere —
     the image simply is not drawn over it. */
  .ch-av { width: 26px; height: 26px; flex: none; background: #000;
    border-radius: 50%; overflow: hidden; }
  .ch-av-img { display: block; width: 100%; height: 100%; object-fit: cover; }
  .ch-av-img.gone { display: none; }
  .ch-card-m { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
  /* Name, badge, amount — in that order and against each other, because that
     is where YouTube puts them. Pushed to the far edge it reads as a caption
     about a screenshot rather than as the screenshot. */
  /* Margins rather than a flex `gap`: the picture reads margins off the
     computed style and has no idea what a gap is, so a gap here comes out as
     a badge welded to the name in every saved card. */
  .ch-card-l1 { display: flex; align-items: center; font-weight: 700; }
  .ch-card-l1 > * + * { margin-left: 5px; }
  .ch-card-l1 .ch-b { margin-right: 0; }
  .ch-card-h .ch-n { color: var(--an); font-weight: 700; }
  .ch-card-l2 { font-weight: 700; }
  .ch-card-l3 { font-size: 0.68rem; opacity: 0.78; }
  .ch-card-x { font-weight: 700; white-space: nowrap; }
  /* The creator's heart WAS here. It is gone because the archive cannot know
     whether it was ever given: the field it was read from is part of YouTube's
     heart BUTTON, not of its state, and ships on every paid message. See the
     note in ls_chat.py's _superchat. */
  .ch-card-h { position: relative; }
  .ch-card-b { background: var(--bb); color: var(--bt); padding: 6px 8px; }
  .ch.card .ch-t { color: inherit; }
  /* A removed superchat still reads as removed, but not in red on red. */
  .ch.card.del .ch-t { color: inherit; text-decoration: line-through; opacity: 0.6; }
  .ch-load { padding: 22px 14px; text-align: center; color: var(--text-3);
    font-size: 0.78rem; }
  .th-chat-note { position: relative; overflow: hidden; flex: none; display: flex;
    gap: 7px; align-items: flex-start; margin: 8px 10px 0; padding: 7px 9px 9px 24px;
    border-radius: 7px; background: rgba(96, 176, 255, 0.11);
    border: 1px solid rgba(96, 176, 255, 0.28);
    color: var(--text-2); font-size: 0.7rem; line-height: 1.45; }
  .th-chat-note > svg { flex: none; width: 13px; height: 13px; margin-top: 1px;
    color: #60b0ff; }
  .th-chat-note[hidden] { display: none; }
  /* Top left, where it is out of the way of the words rather than over them. */
  .th-note-x { position: absolute; top: 3px; left: 4px; width: 15px; height: 15px;
    padding: 0; border: 0; background: transparent; color: var(--text-3);
    font-size: 14px; line-height: 1; cursor: pointer; }
  .th-note-x:hover { color: var(--text); }
  /* Ten seconds, drawn. Nobody has to guess whether it is about to go. */
  .th-note-bar { position: absolute; left: 0; bottom: 0; height: 2px; width: 100%;
    background: #60b0ff; animation: th-note-drain 10s linear forwards; }
  @keyframes th-note-drain { to { width: 0; } }
  @media (prefers-reduced-motion: reduce) { .th-note-bar { animation: none; } }

  .th-chat-find { flex: none; display: flex; align-items: center; gap: 6px;
    margin: 8px 10px 0; padding: 0 8px; border-radius: 7px;
    background: var(--surface-2); }
  .th-chat-find[hidden] { display: none; }
  .th-chat-find > svg { flex: none; width: 13px; height: 13px; color: var(--text-3); }
  .th-chat-find input { flex: 1 1 auto; min-width: 0; border: 0; background: transparent;
    color: var(--text); font: inherit; font-size: 0.72rem; padding: 6px 0; outline: none; }
  .th-chat-find input::-webkit-search-cancel-button { display: none; }
  .th-chat-find button { flex: none; border: 0; background: transparent;
    color: var(--text-3); font-size: 15px; line-height: 1; cursor: pointer; padding: 0 2px; }
  .th-chat-find button:hover { color: var(--text); }
  .ch-found { padding: 7px 10px 4px; color: var(--text-3);
    font-family: var(--font-mono); font-size: 0.56rem; letter-spacing: 0.05em;
    text-transform: uppercase; }
  /* One button, moved to whichever row the cursor is on, rather than one per
     row: there are 240 rows at any moment and 239 of those buttons would exist
     to be invisible. */
  #th-chat { position: relative; }
  .ch-dl { position: absolute; right: 6px; width: 22px; height: 22px; padding: 0;
    display: none; align-items: center; justify-content: center; border: 0;
    border-radius: 5px; cursor: pointer; background: var(--surface-3);
    color: var(--text-2); }
  .ch-dl.on { display: flex; }
  .ch-dl:hover { background: var(--accent); color: #fff; }
  .ch-dl svg { width: 13px; height: 13px; }
  .ch-dl.busy { opacity: 0.5; pointer-events: none; }

  .th-panel-foot { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 10px;
    border-top: 1px solid rgba(255,255,255,0.06); font-family: var(--font-mono);
    font-size: 0.56rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-3); }
  .th-foot-btn { margin-left: auto; background: var(--surface-2); border: 0; color: var(--text-2);
    font-family: var(--font-mono); font-size: 0.56rem; letter-spacing: 0.05em;
    text-transform: uppercase; padding: 4px 8px; border-radius: 6px; cursor: pointer; }
  .th-foot-btn:hover { background: var(--surface-3); color: var(--text); }

  /* ── narrow: panel drops under the stage, the sheet scrolls ── */
  @media (max-width: 1180px) {
    :root { --th-sheet-h: calc(var(--vh) * 96); }
    .th-body { grid-template-columns: minmax(0,1fr); overflow-y: auto; }
    .th-panel { height: 300px; }
    .th-legend { display: none; }
  }

  /* ═══════════════════════════════════════════════════════════════════════
     WRITE — identity, the changeset primitive's feedback, and the tag picker
     ═══════════════════════════════════════════════════════════════════════ */

  /* Who you are. Nothing in this app could write anything before this existed:
     there was no way to establish a session from the browser at all. */
  .who { margin-left: auto; display: flex; align-items: center; gap: 8px; }
  /* ---- the username, and the menu under it ------------------------------
     A BUTTON rather than a span, because it opens something. Opened on CLICK
     and not only on hover: this archive has met that problem before — `H` on
     whatever is hovered opens the history panel and needed a button beside it,
     because hovering does not exist on a phone — and the phone layout is
     shelved as serviceable, which a hover-only control would quietly undo.
     Hover opens it too, because on a pointer that is the gesture that was
     asked for. */
  /* THE WRAPPER is what the menu is positioned against, because the menu is
     its SIBLING and not the button's child — see the note on the markup. */
  .who-wrap { position: relative; display: inline-flex; align-items: center; }
  .who-name { font-size: 0.78rem; color: var(--text-2);
    background: none; border: 0; font-family: inherit; cursor: pointer;
    padding: 3px 6px; border-radius: 7px; }
  .who-name:hover, .who-name[aria-expanded="true"] { background: var(--surface-2);
    color: var(--text); }
  .who-name b { color: var(--text); font-weight: 600; }
  .who-menu { position: absolute; z-index: 60; top: calc(100% + 5px); right: 0;
    min-width: 190px; padding: 4px; border-radius: 10px; text-align: left;
    background: var(--panel); border: 1px solid var(--border);
    box-shadow: 0 10px 26px rgba(0,0,0,0.45); }
  .who-menu[hidden] { display: none; }
  /* A TRANSPARENT BRIDGE ACROSS THE GAP. The menu hangs 5px below the button
     with air between them, and that air belongs to the topbar -- so a pointer
     travelling from the button down to the menu left `.who-wrap` halfway and
     the menu shut before it could be reached. A `::before` hit-tests as the
     element that owns it, so crossing the gap never leaves the wrapper. */
  .who-menu::before { content: ""; position: absolute; left: 0; right: 0;
    top: -7px; height: 7px; }
  .who-menu button { display: block; width: 100%; padding: 7px 9px; border: 0;
    border-radius: 7px; background: transparent; color: var(--text-2);
    font: inherit; font-size: 0.78rem; cursor: pointer; text-align: left; }
  .who-menu button:hover { background: var(--surface-2); color: var(--text); }
  /* The sign-in link, which `.btn-ghost` already shapes — an anchor just
     needs the underline taken off it. */
  .who-in { text-decoration: none; }
  .who-role { font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.06em;
    text-transform: uppercase; padding: 2px 6px; border-radius: 5px;
    background: var(--surface-2); color: var(--text-3); }
  .who-role.can-edit { background: var(--accent-soft); color: var(--accent); }
  .who-note { font-family: var(--font-mono); font-size: 0.55rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--text-3); cursor: help;
    border-bottom: 1px dotted var(--text-3); }
  .who-dev { font-family: var(--font-mono); font-size: 0.52rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: #ffb3b3; background: rgba(255,120,120,0.12);
    border: 1px solid rgba(255,120,120,0.25); padding: 2px 6px; border-radius: 5px; }

  /* Applied or queued. A suggester whose edit silently queues will think it
     vanished, so the difference is said out loud every time. */
  .toasts { position: fixed; right: 18px; bottom: 18px; z-index: 90; display: flex;
    flex-direction: column; gap: 8px; align-items: flex-end; pointer-events: none; }
  .toast { pointer-events: auto; max-width: 380px; padding: 10px 13px; border-radius: 10px;
    font-size: 0.8rem; line-height: 1.45; background: var(--panel); color: var(--text);
    box-shadow: 0 0 0 1px rgba(255,255,255,0.09), 0 14px 40px rgba(0,0,0,0.5);
    animation: toast-in 0.18s ease; }
  .toast b { display: block; font-weight: 600; margin-bottom: 2px; }
  .toast.applied b { color: #7fd6a2; }
  .toast.queued b  { color: var(--air); }
  .toast.bad b     { color: #ffb3b3; }
  .toast small { color: var(--text-3); }
  @keyframes toast-in { from { opacity: 0; transform: translateY(6px); } }

  /* ── tag chips ── */
  .tags-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 9px; }
  .tagchip { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px 3px 3px;
    border-radius: 999px; background: var(--surface-2); border: 1px solid transparent;
    color: var(--text-2); font-size: 0.76rem; cursor: pointer; font-family: inherit; }
  .tagchip:hover { background: var(--surface-3); color: var(--text); }
  .tagchip .art { width: 20px; height: 20px; border-radius: 999px; object-fit: cover;
    background: var(--surface-3); flex: none; display: grid; place-items: center;
    font-family: var(--font-mono); font-size: 0.52rem; color: var(--text-3); overflow: hidden; }
  /* Same four colours as the timeline, at a third of the alpha: a chip and the
     block it labels have to read as one fact, not two. `unknown` gets no tint
     at all — the default border is the absence of a claim. */
  /* Written off the tokens, not as hand-mixed rgba. The literal values here
     were the old palette spelled a second time, so renaming a kind left
     `character` with no rule at all — no tint — and `type` wearing the purple
     that `media` had already taken. One source per colour. */
  /* Two pixels, and only here: a copyright is the franchise a broadcast
     belongs to, which is the one tag on the row that is about WHAT this is
     rather than what is in it. The border is the whole of the distinction, so
     it is the thing that gets heavier.
     The padding gives back the pixel the border takes, or the chip grows and
     the row of them stops lining up. */
  .tagchip.copyright { border-width: 2px; padding: 2px 8px 2px 2px;
    border-color: color-mix(in srgb, var(--seg-copyright) 35%, transparent); }
  .tagchip.character { border-color: color-mix(in srgb, var(--seg-character) 35%, transparent); }
  .tagchip.type      { border-color: color-mix(in srgb, var(--seg-type) 35%, transparent); }
  .tagchip.elements  { border-color: color-mix(in srgb, var(--seg-elements) 55%, transparent); }
  .tagchip.meta      { border-color: color-mix(in srgb, var(--seg-meta) 35%, transparent); }
  .tagchip.artist    { border-color: color-mix(in srgb, var(--seg-artist) 45%, transparent); }
  .tagchip.general   { border-color: color-mix(in srgb, var(--seg-general) 55%, transparent); }
  /* Not in anyone's autocomplete yet — visibly provisional rather than silently
     different from the confirmed ones next to it. */
  .tagchip.proposed { border-style: dashed; border-color: var(--text-3); opacity: 0.85; }
  .tagchip.proposed::after { content: 'proposed'; font-family: var(--font-mono);
    font-size: 0.5rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-3); }
  .tagchip .parent { color: var(--text-3); }
  /* The two pills were one button at two sizes — same radius, same hover,
     and nothing between them but how much of it there is. `.btn-add` is both
     now; these two rules say only the size. */
  .tag-add { width: 26px; height: 26px; border-radius: 999px;
    display: grid; place-items: center; font-size: 0.95rem; line-height: 1;
    padding: 0; }
  .tag-add:disabled { opacity: 0.3; cursor: not-allowed; }
  .tags-empty { font-size: 0.74rem; color: var(--text-3); }

  /* ── the picker / editor popover ──
     Rendered into #sheet or #th rather than <body>, because the page closes the
     sheet on any click that is not inside it — a popover on <body> would
     dismiss the thing it is editing the moment you clicked into it. */
  /* Above everything it can be opened from, which is now more than it was: the
     upload window is 92 and the Add-a-song window shares that rule, so at 80
     the taglet picker opened from inside one rendered BEHIND it — a list you
     could see the top edge of and nothing else. 96 clears the windows (92),
     the embed (90) and the tag panel (94), and it is the topmost thing in the
     page because a picker is always the most recent thing you asked for. */
  /* Everything that makes it glass is on `.glass, .pop2` up in the upload
     window's section — the one recipe, shared rather than restated. What is
     left here is geometry, and the four popover elements (pop2, pop3, pop4,
     pop5) all carry the `pop2` class, so they all get it.
     ONE backdrop-filter, on the shell, and none on the rows: roadmap.md names
     a per-card backdrop-filter as the leading suspect for the CPU a long-open
     tab burns, and a popover that exists only while it is open is the cheapest
     possible place to spend one. */
  .pop2 { position: fixed; z-index: 96; width: 290px; box-sizing: border-box;
    border-radius: 12px; padding: 8px; font-size: 0.82rem; }
  .pop2[hidden] { display: none; }

  /* ---- what the Contact button opens -------------------------------------
     Prose in a popover built for pickers, so the only work here is making a
     paragraph read like one. It stays at `.pop2`'s 290px rather than widening
     for itself: `openPop` clamps the left edge against that number, and this
     button sits at the right end of the bar, which is exactly where a wider
     box would hang off the screen. */
  .ct-pop { padding: 3px 4px 4px; }
  .ct-pop p { margin: 0 0 9px; color: var(--text-2); line-height: 1.45;
    font-size: 0.78rem; }
  /* The two ways to reach somebody, shaped alike so neither reads as the
     afterthought: mark, address, and the copy that a handle needs because a
     Discord handle is not a link anybody can follow. */
  .ct-row { display: flex; align-items: center; gap: 8px; padding: 6px 7px;
    border-radius: 9px; background: var(--glass-sunk); margin-top: 5px;
    color: var(--text); font-size: 0.76rem; }
  .ct-row:hover { background: var(--surface-2); }
  .ct-row svg { flex: none; color: var(--text-3); }
  /* The address itself, whether or not it is a link. A `mailto:` opens
     nothing on a machine with no mail client, which is why the copy sits
     beside it rather than instead of it.
     `ct-mail` AND NOT `ct-ad`, which is what this was: every cosmetic filter
     list matches a name ending in `-ad`, so the contact address would have
     been invisible to each reader running a blocker — and invisible is the
     one thing a removal notice must never be. `blockers.mjs` caught it. */
  .ct-row span, .ct-row .ct-mail { min-width: 0; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; color: var(--text);
    text-decoration: none; font-family: var(--font-mono); font-size: 0.72rem; }
  .ct-row .ct-mail:hover { color: var(--accent); }
  .ct-copy { margin-left: auto; flex: none; border: 1px solid var(--border);
    background: none; color: var(--text-3); font: inherit; font-size: 0.66rem;
    padding: 2px 7px; border-radius: 7px; cursor: pointer; }
  .ct-copy:hover { color: var(--text); background: var(--surface-2); }
  .pop2 input, .pop2 select, .pop2 textarea { width: 100%; box-sizing: border-box;
    background: var(--glass-sunk);
    border: 1px solid var(--glass-line); color: var(--text); border-radius: 9px;
    padding: 7px 9px; font-family: inherit; font-size: 0.82rem; }
  .pop2 input:focus, .pop2 select:focus, .pop2 textarea:focus {
    outline: none; border-color: var(--accent); }
  /* The editor's line grows the same way the add field does — it is the same
     line, opened for correction, and reading it through a one-line slit is
     what made a typo hard to find in the first place. */
  .pop2 textarea { resize: none; overflow: hidden; line-height: 1.45;
    font-family: inherit; }
  /* .up-ac's box, to the pixel: a well under the field rather than more of the
     same surface. The list is the one part of a picker that is the archive's
     own rows and not chrome, and the inset is what says so. */
  .pop2-list { margin-top: 6px; max-height: 250px; overflow-y: auto;
    background: rgba(0,0,0,0.2); border: 1px solid var(--glass-line);
    border-radius: 9px; padding: 3px; }
  /* The kind, as a lit left edge — the same three pixels .up-ac-row uses, so
     a row means the same thing in the popover as it does in the upload window.
     --seg-kind defaults to a neutral rather than to transparent: an unfiled
     row wants a grey edge, not a missing one, and color-mix() on transparent
     would leave the selected state invisible. */
  .pop2-row { display: flex; align-items: center; gap: 8px; padding: 6px 7px;
    border-radius: 8px; cursor: pointer; --seg-kind: var(--text-3);
    box-shadow: inset 3px 0 0 var(--seg-kind); }
  .pop2-row[data-kind="copyright"] { --seg-kind: var(--seg-copyright); }
  .pop2-row[data-kind="character"] { --seg-kind: var(--seg-character); }
  .pop2-row[data-kind="type"]      { --seg-kind: var(--seg-type); }
  .pop2-row[data-kind="elements"]  { --seg-kind: var(--seg-elements); }
  .pop2-row[data-kind="meta"]      { --seg-kind: var(--seg-meta); }
  .pop2-row[data-kind="artist"]    { --seg-kind: var(--seg-artist); }
  .pop2-row[data-kind="general"]   { --seg-kind: var(--seg-general); }
  .pop2-row[aria-selected="true"] {
    background: color-mix(in srgb, var(--seg-kind) 24%, transparent); }
  .pop2-row .art { width: 22px; height: 22px; border-radius: 5px; flex: none;
    background: var(--surface-3); object-fit: cover; }
  .pop2-row .nm { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  .pop2-row .n { font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-3);
    flex: none; }
  .pop2-row .sub { font-family: var(--font-mono); font-size: 0.56rem; color: var(--text-3); }
  /* Creating is always a deliberate click, never the consequence of not
     picking something. Typing "mario kart" where "Mario Kart 8 Deluxe" already
     exists should not quietly mint a second one. */
  .pop2-row.mint { color: var(--accent); --seg-kind: var(--accent); }
  /* Suggested and not yet approved — its author's own row, and nobody else is
     ever served one. The lit left edge goes dashed, because a solid one is
     this page's mark for "this is vocabulary" and that is the claim being
     withheld. The kind colour stays: the category is not what is in doubt. */
  /* ── A NEAR MISS IS NOT A MATCH AND MUST NOT READ AS ONE ────────────────
     These are offered because `/api/tags` is a substring search and a single
     edit anywhere defeats it — see the near pass in `makePicker`. They are the
     archive guessing, which is a useful thing to be shown and a dangerous
     thing to mistake for an answer: picking the wrong artist here aliases a
     booru key to somebody else's name, and the alias is what every later
     import of that key then believes. Dimmed, and carrying the distance in
     words. */
  .pop2-row.near { opacity: 0.72; }
  .pop2-row.near .nm { font-style: italic; }
  .pop2-row.near[aria-selected="true"] { opacity: 1; }
  .pop2-row.pend { box-shadow: none;
    border-left: 3px dashed color-mix(in srgb, var(--seg-kind) 70%, transparent);
    background: color-mix(in srgb, var(--seg-kind) 7%, transparent); }
  /* The colour of removal, and last in the list. Not a warning — dismissing a
     suggestion destroys nothing, it answers a question — but it is the one row
     here that is a no, and it should not be reachable by accident on the way
     to the mint row above it. */
  .pop2-row.drop { color: var(--tl-not); --seg-kind: var(--tl-not); }
  .pop2-row.drop .art { display: grid; place-items: center; color: var(--tl-not); }
  /* Filing the new tag is one click, on the row below it, instead of a guess.
     The old code minted everything as 'game' because most tags are games —
     which is the same move as reading a category off a title, just quieter. */
  .pop2-kinds { display: flex; flex-wrap: wrap; gap: 4px; padding: 2px 7px 7px 37px; }
  .pop2-kinds button { display: inline-flex; align-items: center; gap: 4px; border: 0;
    background: var(--glass-2); color: var(--text-2); border-radius: 6px;
    padding: 4px 7px; cursor: pointer; font-family: var(--font-mono);
    font-size: 0.56rem; letter-spacing: 0.05em; text-transform: uppercase; }
  .pop2-kinds button:hover { background: var(--glass-hi); color: var(--text); }
  .pop2-kinds button i { width: 6px; height: 6px; border-radius: 2px; }
  /* Which one Enter files it as, shown instead of said.
     There was a sentence here — "…or just Enter, and file it later" — and it
     was explaining a default that no longer exists: nothing mints unfiled any
     more. So the row shows the answer on the chip, and the prose goes.
     Not aria-pressed: these buttons COMMIT on click rather than select, and a
     pressed state on a button that fires is a lie to whatever reads it out. */
  .pop2-kinds button.dflt { background: var(--glass-hi); color: var(--text);
    box-shadow: inset 0 0 0 1px var(--glass-line); }
  .pop2-row.mint .art { display: grid; place-items: center; color: var(--accent);
    background: var(--accent-soft); font-size: 0.9rem; line-height: 1; }
  .pop2-empty { padding: 14px 8px; text-align: center; color: var(--text-3); font-size: 0.76rem; }
  .pop2-sec { margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.07); }
  .pop2-lab { font-family: var(--font-mono); font-size: 0.54rem; letter-spacing: 0.07em;
    text-transform: uppercase; color: var(--text-3); margin-bottom: 5px; display: block; }
  .pop2-btns { display: flex; gap: 6px; margin-top: 8px; }
  .pop2-btns button { flex: 1 1 auto; border: 0; border-radius: 8px; padding: 7px;
    font-family: inherit; font-size: 0.78rem; font-weight: 600; cursor: pointer;
    background: var(--glass-2); color: var(--text-2); }
  .pop2-btns button:hover { background: var(--glass-hi); color: var(--text); }
  .pop2-btns button.primary { background: var(--accent); color: var(--on-accent); }
  /* AN ANCHOR IN THIS ROW IS A DOWNLOAD and has to stay one — a button that
     sets `location` is a link with middle-click and save-as taken away. So it
     is given the button's clothes rather than left reading as prose, which is
     what the first .torrent link in here looked like. */
  .pop2-btns a { flex: 1 1 auto; border-radius: 8px; padding: 7px;
    font-size: 0.78rem; font-weight: 600; line-height: 1.2; text-align: center;
    text-decoration: none; background: var(--glass-2); color: var(--text-2); }
  .pop2-btns a:hover { background: var(--glass-hi); color: var(--text); }
  /* A refusing button that looks ready to be pressed is the interface telling
     one story and the code telling another. The error line above it says why. */
  .pop2-btns button:disabled { opacity: 0.4; cursor: not-allowed; }
  .pop2-btns button:disabled:hover { background: var(--glass-2); color: var(--text-2); }
  .pop2-btns button.primary:disabled:hover { background: var(--accent); color: var(--on-accent); }
  /* Two scopes, kept visually apart on purpose: one removes a row, the other
     retracts a thing that other streams are using. */
  .pop2-danger { color: #ffb3b3 !important; }
  .pop2-danger:hover { background: rgba(255,120,120,0.14) !important; color: #ffc9c9 !important; }
  .pop2-scope { font-size: 0.68rem; color: var(--text-3); margin-top: 6px; line-height: 1.45; }

  /* ── the note form ─────────────────────────────────────────────────────
     One text field, because a note is one line. Everything under it is the
     parse shown back — nothing about splitting a line into four columns should
     ever be a surprise. */
  .pop4 { width: 340px; }
  .pop5 { width: 424px; }   /* the upload window's width, for the same reason */
  .nte-read { margin-top: 7px; font-size: 0.76rem; line-height: 1.5; color: var(--text-2); }
  .nte-read .nte-tag { font-family: var(--font-mono); font-size: 0.64rem; color: var(--air);
    font-weight: 400; }
  .nte-read .nte-seq { font-family: var(--font-mono); font-size: 0.64rem; color: var(--text-3);
    font-weight: 400; }
  .nte-sep { color: var(--text-3); margin: 0 5px; }
  .nte-none { color: var(--text-3); font-style: italic; }
  .nte-clock { display: flex; align-items: center; gap: 5px; margin-top: 7px;
    font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-3);
    flex-wrap: wrap; }
  .nte-clock b { color: var(--text); font-variant-numeric: tabular-nums; }
  /* The icon sits in front of the timestamp and opens the list. Small on
     purpose: it is a correction you make occasionally, not a field you fill. */
  .nte-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; padding: 0;
    border: 0; cursor: pointer; }
  button.nte-dot:hover { box-shadow: 0 0 0 2px var(--surface-3); }
  .nte-dot.yt { background: #ff4444; }
  .nte-dot.tw { background: #a970ff; }
  .nte-dot.local { background: var(--air); }
  .nte-dot.axis { background: var(--accent); }
  .nte-dot.unset { background: transparent; box-shadow: inset 0 0 0 1.2px var(--text-3); }
  .nte-caret { margin-left: -2px; opacity: 0.6; }
  /* The second point of a span, carried but never read. A note is a pin. */
  .nte-extra { opacity: 0.6; }
  .nte-axis { color: var(--accent); }
  .nte-list { margin-top: 5px; display: flex; flex-direction: column; gap: 1px; }
  .nte-list button { display: flex; align-items: center; gap: 7px; border: 0; cursor: pointer;
    background: transparent; color: var(--text-2); border-radius: 6px; padding: 5px 7px;
    font-family: var(--font-mono); font-size: 0.6rem; text-align: left; }
  .nte-list button:hover:not(:disabled) { background: var(--surface-2); color: var(--text); }
  .nte-list button[aria-selected="true"] { background: var(--surface-2); color: var(--text); }
  .nte-list button:disabled { opacity: 0.4; cursor: not-allowed; }
  .nte-list .nte-dot { cursor: default; }
  /* The line this was imported from, never edited. It is the receipt a
     correction can be checked against. */
  .nte-raw { margin-top: 8px; font-size: 0.62rem; color: var(--text-3); line-height: 1.5; }
  .nte-raw code { font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-3);
    background: var(--surface-2); border-radius: 4px; padding: 1px 4px; word-break: break-all; }

  /* ── the chapter form ──────────────────────────────────────────────────
     Same skin as the tag popover, wider, and it owns its own element so the
     tag autocomplete inside it is not fighting the one outside it. */
  .pop3 { width: 330px; }
  .ch-times { display: flex; align-items: flex-end; gap: 6px; }
  .ch-times label { flex: 1 1 0; font-family: var(--font-mono); font-size: 0.54rem;
    letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-3); }
  .ch-times input { font-family: var(--font-mono); font-variant-numeric: tabular-nums;
    text-align: center; margin-top: 3px; }
  .ch-dur { flex: none; font-family: var(--font-mono); font-size: 0.6rem;
    color: var(--text-3); padding-bottom: 8px; }
  .ch-tag { display: flex; align-items: center; gap: 6px; margin-top: 8px;
    background: var(--surface-2); border-radius: 8px; padding: 4px 6px 4px 4px; }
  .ch-tag .art { width: 20px; height: 20px; border-radius: 999px; object-fit: cover;
    background: var(--surface-3); flex: none; display: grid; place-items: center;
    font-family: var(--font-mono); font-size: 0.5rem; color: var(--text-3); overflow: hidden; }
  .ch-tag .nm { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; font-size: 0.8rem; }
  .ch-tag[hidden] { display: none; }
  .ch-tag .sub { font-family: var(--font-mono); font-size: 0.56rem; color: var(--accent); }
  .ch-tag button { border: 0; background: transparent; color: var(--text-3); cursor: pointer;
    font-size: 0.9rem; line-height: 1; padding: 2px 4px; }
  .ch-tag button:hover { color: var(--text); }
  .pop3 .pop2-list { max-height: 168px; }
  /* The inner lane's single field, plus the line that says what it will land
     inside. That line is computed from the times on every keystroke, because
     nothing stores the relationship — see projectSegments. */
  .pop3 input[data-f="label"] { margin-top: 5px; }
  .pop3 [data-ch-under]:empty { display: none; }
  .ch-err { color: #ffb3b3; font-size: 0.7rem; margin-top: 8px; line-height: 1.45; }
  .ch-err:empty { display: none; }
  .ch-hint { color: var(--text-3); font-size: 0.66rem; margin-top: 6px; line-height: 1.45; }
  .pop2-kinds button[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); }
  .pop3 .pop2-kinds { padding: 4px 0 0; }

  @media (max-width: 700px) {
    :root { --th-strip: 78px; }
    .th-meta, .th-src { display: none; }
  }

  /* A picture in the clip list, which happens in the Review queue and in a
     search that crosses collections. The frame keeps its layout; what goes is
     the hover-grow, because there is nothing underneath the poster to reveal —
     the poster IS the picture — and a still that lurched under the cursor for
     no payoff would just be noise in a queue somebody is working through. */
  .snip.is-still .snip-frame { cursor: default; }
  .snip.is-still:hover .snip-frame img { transform: none; }
  .snip.is-still .snip-frame img { object-fit: contain;
    background: color-mix(in srgb, var(--bg) 55%, transparent); }
  /* Which collection a queued row will go back to. Only ever drawn on a row
     that is NOT a snippet, so the common case carries no badge at all. */
  .snip[data-kind]::after { content: attr(data-kind); position: absolute;
    left: 8px; top: 8px; z-index: 4; font-size: 9px; font-weight: 700;
    letter-spacing: .04em; text-transform: uppercase; padding: 2px 5px;
    border-radius: 5px; pointer-events: none;
    background: color-mix(in srgb, var(--panel) 86%, transparent);
    border: 1px solid color-mix(in srgb, var(--surface-3) 70%, transparent);
    color: var(--text-3); }
  /* AND THE SAME CORNER AS A BUTTON, on a row where the collection can still
     be changed. A pseudo-element cannot take a click — the rule above says
     `pointer-events: none` and could not say anything else — so a switchable
     row gets a real element instead and this one steps aside for it. Not both:
     one label, one source, because two things drawing one fact is how they
     come to disagree. The face is copied from the rule above deliberately, so
     a row that can be switched looks identical until it is pointed at. */
  .snip:has(.snip-kind)::after { content: none; }
  .snip-kind { position: absolute; left: 8px; top: 8px; z-index: 4;
    display: inline-flex; align-items: center; gap: 3px; cursor: pointer;
    font-family: inherit; font-size: 9px; font-weight: 700;
    letter-spacing: .04em; text-transform: uppercase; padding: 2px 5px;
    border-radius: 5px;
    background: color-mix(in srgb, var(--panel) 86%, transparent);
    border: 1px solid color-mix(in srgb, var(--surface-3) 70%, transparent);
    color: var(--text-3); }
  .snip-kind svg { width: 9px; height: 9px; flex: none; opacity: 0.75; }
  .snip-kind:hover, .snip-kind:focus-visible { color: var(--text);
    border-color: color-mix(in srgb, var(--accent) 70%, transparent); }
  .snip-kind:hover svg, .snip-kind:focus-visible svg { opacity: 1; }
  /* In flight. `progress` and not `not-allowed`: the press was accepted. */
  .snip-kind[disabled] { cursor: progress; opacity: 0.55; }

  /* ══ pictures — memes and the gallery ═══════════════════════════════════
     Two panels, one component. The difference between them is a word in a
     query string, so anything that reads as a rule about "memes" here is
     really a rule about pictures, and the gallery gets it for free.

     The tile is ONE FIXED RATIO, because a grid of pictures whose rows are
     all different heights is a grid nobody can scan — the eye needs a lattice
     to move along. The crop is for looking, not a claim about the picture:
     everything below exists to give the whole thing back the moment you point
     at it.

     0.78 rather than 1, and the gap 6px rather than 10px. Both were measured
     on readers rather than reasoned about here, and the portrait tile won
     comfortably — which is not surprising once said out loud, since a
     reaction face and a screenshot of a chat are both taller than they are
     wide. Nothing in the component depends on the number: `object-fit: cover`
     crops to whatever the lattice is, and the hover gives the real shape
     back. Change it here and the whole grid follows. */
  /* AND THE GAP IS 32px NOW, which supersedes the number in the note above
     without touching what it measured. 6px was chosen against the TILE — the
     ratio that reads best — and that reading still holds; what it was never
     asked about is the FIELD, which is a different question answered by
     living in it. Reported as claustrophobic, and as not having been noticed
     until it was. A grid this tight reads as one texture rather than as
     pictures: the eye has nothing to stop on between them, which is the whole
     job of a gap. Columns are `auto-fill` over a 168px floor, so the room
     comes out of the column COUNT and never out of a tile. */
  /* THE DECK'S FIVE NUMBERS, NAMED. They were literals, and every one of them
     was chosen against a 174px tile — the note below `.pic-deck` says so in
     as many words about the fourteen. A smaller tile with the same mat keeps
     more paper around less picture, so the default view below re-derives them
     at 150/174 rather than leaving the stack to grow into the thumbnail.
     The fallbacks are the originals, so a `.pic` outside a `.pics` is drawn
     exactly as it was. */
  .pics { display: grid; gap: 32px; padding: 2px 0 22px;
          grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
          --pdk-c1: 4px; --pdk-c2: 8px; --pdk-mat: 14px; --pdk-drop: 11px;
          --pdk-pin: 18px; }

  /* ---- the default view ---------------------------------------------------
     150 and 24 against theater's 168 and 32. On a 1920 screen that is nine
     columns of 150 where there were nine of 174, and 9x24 + 8x8 = 280px of
     room — left where the rail will go, and empty until it does.
     The tile keeps `aspect-ratio: 0.78`, so a row comes down from 258px to
     219 as well: more of the lattice on screen is most of what this is for,
     and it is the vertical half nobody asks for by name. */
  /* 218 of rail, 32 of gap and 30 of air at the far edge is the 280 the
     resize freed, with the grid landing on exactly the width it had before
     the air was taken out of it: nine columns of 150 on a 1920 screen either
     way. The air is why the rail is 218 and not 248 — the lattice wants a
     margin on the side it ends at, and the room for it came out of the rail
     rather than out of a column. */
  #panel-images { --gal-rail: 218px; --gal-rail-gap: 32px; --gal-rail-air: 30px; }
  #panel-images.is-default { display: grid; align-items: start;
    grid-template-columns: var(--gal-rail) minmax(0, 1fr);
    column-gap: var(--gal-rail-gap); }
  /* The bar spans both: the switch, the search and the tab stay exactly where
     they are in either view, which is the whole of "theater unchanged". */
  #panel-images.is-default > .toolbar,
  #panel-images.is-default > .snip-meta-row,
  #panel-images.is-default > #gal-more { grid-column: 1 / -1; }
  #panel-images.is-default > .gal-rail { grid-column: 1; }
  #panel-images.is-default > .pics { grid-column: 2; gap: 24px;
    margin-right: var(--gal-rail-air);
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    --pdk-c1: 3px; --pdk-c2: 7px; --pdk-mat: 12px; --pdk-drop: 9px;
    --pdk-pin: 15px; }

  /* NO PANELS AND NO HEADINGS. Two groups on the page with space between
     them: a card around each is two more edges beside a lattice that is
     nothing but edges, and a heading over four shelves is a word explaining
     a picture that is already clear. */
  .gal-rail { display: flex; flex-direction: column; gap: 26px;
              position: sticky; top: 14px; min-width: 0; }

  /* THE SHELVES, STOOD UP — and it is the tools panel's tablist rather than
     the segmented control turned on its side. A segmented control is a
     horizontal device, one track cut into pills, and rotating it gives five
     stacked pills that no longer look cut from anything; `.tp-side` is the
     archive's own answer to this exact shape and a reader who learned it
     under Tools has learned it here. */
  #gal-rail .seg.gal-view { display: flex; flex-direction: column; gap: 2px;
    padding: 0; border: 0; background: none; border-radius: 0; }
  #gal-rail .seg.gal-view button { display: flex; align-items: center; gap: 9px;
    width: 100%; text-align: left; padding: 9px 10px; border-radius: 9px;
    font-size: 0.82rem; font-weight: 500; color: var(--text-2);
    box-shadow: inset 2px 0 0 transparent; }
  #gal-rail .seg.gal-view button:hover { background: var(--surface-2); color: var(--text); }
  #gal-rail .seg.gal-view button[aria-pressed="true"] { background: var(--accent-soft);
    color: var(--text); box-shadow: inset 2px 0 0 var(--accent); }
  /* The rule turns with the list: "and also" reads across a row and down a
     column, but not as the same pixel. */
  #gal-rail .seg-rule { width: auto; height: 1px; margin: 7px 2px; }
  .gal-view .ga-l { display: none; }
  #gal-rail .gal-view .ga-s { display: none; }
  #gal-rail .gal-view .ga-l { display: inline; }

  /* ── THE PINS ─────────────────────────────────────────────────────────
     A world you are looking at, drawn as a thing you put there rather than
     listed: filled in its tag's category colour when it is lit, and the same
     hue taken most of the way down to the page when it is not. FILLED AND NOT
     WASHED — a wash reads as "disabled" where this means "mine, not asking"
     — and no outline, because an outline is another edge beside a grid full of
     them. The solid pair is also the only version that survives being glanced
     at from across the lattice, which is the job. */
  .gal-pins { display: flex; flex-direction: column; gap: 5px; }
  .pin-wrap { position: relative; display: flex; }
  .pin { position: relative; flex: 1; min-width: 0; display: flex;
    align-items: center; gap: 7px; height: 30px; padding: 0 10px 0 11px;
    border: 0; border-radius: 3px 9px 9px 3px; cursor: pointer;
    font: inherit; font-size: 0.78rem; text-align: left; color: var(--text-3);
    background: color-mix(in srgb, rgb(var(--pg)) 17%, var(--bg));
    transition: background 0.16s ease, color 0.16s ease; }
  /* The head, lit only when the pin is lit — the same edge the shelf list
     above it uses to say "this is the one". */
  .pin::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px;
    border-radius: 3px 0 0 3px; background: rgb(var(--pg)); opacity: 0;
    transition: opacity 0.16s ease; }
  /* LIT, AND THE RIGHT EDGE IS WHAT MOVES. Sliding the whole pin indented the
     end it goes into, which read as pushed further in; growing it leaves the
     head where it is and lets the pin protrude into the gap.
     THE PROTRUSION IS THE ROW'S, not the button's. On the button it stopped at
     the chevron, so a lit pin with children sat flush while a lit one without
     stuck out — two widths for one state, which reads as a bug rather than as
     a device. The wrap grows and everything in it goes along. */
  .pin[aria-pressed="true"] { color: #fff; font-weight: 600;
    background: color-mix(in srgb, rgb(var(--pg)) 68%, var(--bg)); }
  .pin-wrap:has(.pin[aria-pressed="true"]) { margin-right: -6px;
    transition: margin-right 0.16s cubic-bezier(0.22, 1, 0.36, 1); }
  .pin[aria-pressed="true"]::before { opacity: 1; }
  .pin:hover { color: var(--text); }
  .pin[aria-pressed="true"]:hover {
    background: color-mix(in srgb, rgb(var(--pg)) 78%, var(--bg)); }
  .pin-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* THE CHIP'S OWN VEIL, REUSED AND NOT REDRAWN. `.tl-x` is the red wipe a
     filter pill wears, down to the struck-through name under it; taking a pin
     off the board is the same promise about the same kind of object, so it
     gets the same answer from the same rule rather than one that drifts. */
  .pin-wrap:hover .tl-x { opacity: 1; pointer-events: auto; }
  .pin-wrap:has(.tl-x:hover) .pin-name { text-decoration: line-through; }
  /* AND IT IS CLIPPED TO THE PILL IT VEILS. `.tl-x` is a square laid over the
     right end of a thing with a 9px corner, and the wrap it is positioned in
     has no shape of its own — so it drew past the pin into the gap, which is
     the tell that it was sitting somewhere nobody had measured. */
  .pin-wrap .tl-x { border-radius: 0 9px 9px 0; }
  /* THE EMPTY SLOT, AT THE PINS' OWN HEIGHT. Two pixels shorter read as a
     different kind of row rather than as a space one of these goes in; the
     dash is what says empty, and it does not need the height to help. */
  /* `flex: none` is not tidiness. The filled pins sit inside a `.pin-wrap`, so
     `.pin`'s `flex: 1` works across the row; this one is a direct child of the
     column, where the same declaration is a `flex-basis: 0` on its HEIGHT —
     and a basis beats the `height` beside it. It drew 21px. */
  .pin-add { flex: none; height: 30px; background: none; color: var(--text-3);
    border: 1px dashed var(--border); }
  .pin-add:hover { color: var(--text-2); border-color: var(--text-3); }
  .pin-add svg { width: 12px; height: 12px; flex: none; }

  /* THE CHEVRON IS A WAY TO LOOK DOWN, NOT A THIRD STATE. What is under a pin
     is a place to browse and add from; ticking a child would mean working out
     which slugs to send for a half-ticked parent, and sending the children
     instead of the parent hides a crossover picture nobody asked to hide. */
  .pin-exp { flex: none; width: 24px; display: grid; place-items: center;
    padding: 0; border: 0; border-radius: 0 9px 9px 0; background: none;
    color: var(--text-3); cursor: pointer; }
  .pin-exp:hover { color: var(--text); }
  .pin-exp svg { width: 11px; height: 11px;
    transition: transform 0.16s cubic-bezier(0.22, 1, 0.36, 1); }
  .pin-exp[aria-expanded="true"] svg { transform: rotate(90deg); }
  /* AND THE VEIL STOPS WHERE THE PIN DOES. `.tl-x` is absolute against the
     WRAP, so `right: 0` is the end of the row rather than the end of the
     pill — which on a row with a chevron is the chevron. It covered it
     exactly, and since both only appear on hover the chevron could not be
     pressed at all. Inset by the chevron's own width, the veil lands back on
     the pin's right end where it belongs and the two never meet. */
  .pin-wrap:has(.pin-exp) .tl-x { right: 24px; }

  /* WHAT IS UNDER AN OPEN PIN. Quieter than the board and indented, because
     each row ADDS rather than toggles — a control that puts something on the
     board should not look like one that is already on it. */
  /* THE RULE IS WHAT TIES THEM TO THE PIN ABOVE. Indented alone they read as
     a second list that happened to appear, which at a glance is the one thing
     they are not: they are what is under THAT one. */
  .pin-kids { display: flex; flex-direction: column; gap: 1px;
    margin: 3px 0 5px 11px; padding-left: 9px;
    border-left: 1px solid var(--border); }
  .pin-kid { display: flex; align-items: center; justify-content: space-between;
    gap: 8px; padding: 6px 9px; border: 0; border-radius: 7px; cursor: pointer;
    font: inherit; font-size: 0.76rem; text-align: left;
    background: none; color: var(--text-3); }
  .pin-kid:hover { background: var(--surface-2); color: var(--text); }
  .pin-kid:disabled { cursor: default; opacity: 0.55; }
  .pin-kid:disabled:hover { background: none; color: var(--text-3); }
  .pin-kid-on { flex: none; font-size: 0.68rem; }
  .pin-kid-none { padding: 6px 9px; font-size: 0.74rem; color: var(--text-3); }

  @media (max-width: 1180px) {
    /* The rail becomes a strip above the grid rather than a narrower column:
       at this width the lattice is down to four or five tiles across and
       218px of it is a third of the page. */
    #panel-images.is-default { grid-template-columns: minmax(0, 1fr); }
    #panel-images.is-default > .gal-rail { grid-column: 1 / -1; }
    #panel-images.is-default > .pics { grid-column: 1 / -1; margin-right: 0; }
    .gal-rail { position: static; flex-direction: row; flex-wrap: wrap; gap: 14px; }
    .gal-rail > * { flex: 1 1 240px; }
    #gal-rail .seg.gal-view { flex-direction: row; flex-wrap: wrap; }
    #gal-rail .seg.gal-view button { width: auto; }
    #gal-rail .seg-rule { width: 1px; height: auto; align-self: stretch; }
  }
  .pics-empty { grid-column: 1 / -1; padding: 42px 0; text-align: center;
                color: var(--text-3); font-size: 13px; }

  /* `margin: 0` is not tidiness. A <figure> carries `margin: 1em 40px` from
     the UA stylesheet, which took 80px out of every grid track — a 155px
     column drew a 75px tile — and left the whole lattice half the size it was
     asked for, with the buttons measuring from the wrong box. */
  .pic { position: relative; aspect-ratio: 0.78; margin: 0; border-radius: 11px;
         background: var(--surface-2); border: 1px solid var(--surface-3);
         cursor: zoom-in; }
  /* Not `overflow: hidden` on the tile. The hover has to escape it — that is
     the entire trick — so the CROP lives on an inner box and the tile itself
     stays open. */
  /* ── one of a sequence ───────────────────────────────────────
     The tile's OWN border, lit, plus a haze off it. Not a stack of paper:
     this was twice an offset sheet drawn with `box-shadow`, and both times it
     read as damage rather than as depth -- the first in the tile's own two
     colours (a card the colour of the card in front of it), the second in
     stepped hairlines, which turned every pooled tile in the grid into a
     smear of edges you had to stop and parse. A lattice of square tiles is
     scanned, not read, and anything that changes a tile's SILHOUETTE fights
     the lattice. A glow leaves the silhouette alone.

     Thin: one pixel of border and a soft 8px haze. It used to be stated as
     fitting inside the grid's 10px `gap` so no two tiles' marks could touch;
     the gap is 6px now and an 8px haze reaches across it, which is why the
     bleed rule below is doing the work rather than the arithmetic. The hover pushes the haze and
     hardens the border, and may bleed a little into the neighbour -- `.pic`
     already takes `z-index: 6` on hover, so it bleeds over, not under. */
  .pic.is-pooled { border-color: var(--pool-lit);
    box-shadow: 0 0 0 1px var(--pool-haze), 0 0 8px 0 var(--pool-haze); }

  /* ── A SEQUENCE, AS ONE TILE ────────────────────────────────────────────
     INSIDE THE BOX, which is the rule stated above this: a lattice is scanned
     rather than read, and anything that changes a tile's SILHOUETTE fights
     it. So the face moves up and left by six pixels and the cards behind fill
     that back out — the outline is the same rectangle the grid always had,
     and only its contents are stacked.
     AND THE CARDS ARE EMPTY. Two slivers of paper, three pixels each, in the
     tile's own surface colours. They are there to say THERE ARE MORE, and a
     thumbnail at that size says nothing a stack of edges does not; it only
     adds two more pictures to a grid that is already a wall of them.
     MARKUP AND NOT `::before`/`::after`, because the face lives inside
     `.pic-link` and `::after` would paint over it — fixable with a z-index on
     the link, which would also make a stacking context the grown hover has to
     escape. A span before the link is behind it by document order, with
     nothing to reason about. */
  .pic-deck { position: absolute; inset: 0; border-radius: inherit;
    pointer-events: none; }
  /* FILLED, NOT OUTLINED, AND FANNED. Two earlier versions got this wrong in
     opposite ways: the first gave each card the tile's own background and a
     one-pixel border, which on a dark page is a dark sliver edged in a
     slightly less dark one — two faint LINES rather than two cards; the
     second made them surfaces but left them square and parallel, which reads
     as a shadow that forgot to blur.
     A PILE OF PRINTS IS NOT A NEAT STACK. Two degrees of rotation is the
     whole difference between "there is something behind this" and "somebody
     put these down", and it costs one transform each.
     THREE PIXELS, WHICH IS AS LITTLE AS THIS CAN SAY. The lattice is scanned
     rather than read and the note on `.pic` above is about exactly this: the
     stack lives INSIDE the cell the grid already allotted, so the face gives
     up six pixels on two sides and nothing anywhere changes shape. */
  /* ONE GRADIENT, CARRIED BY EACH CARD RATHER THAN PAINTED ACROSS THE PILE.
     Each sheet has its own copy, so the two degrees of rotation turn the
     highlight WITH the sheet — which is what makes three of them read as
     three sheets instead of one shape with steps cut into it. A single
     gradient on the container would have lit them as one object and undone
     the fan above. */
  .pic-deck i { position: absolute; border-radius: 9px;
    background: var(--deck-paper); transform-origin: 50% 50%;
    /* Their own, so two rotated edges four pixels apart are two edges. */
    box-shadow: 1px 2px 5px -2px rgba(0, 0, 0, .8); }
  /* ── CONCENTRIC, NOT LEANING ───────────────────────────────────────────
     The first version held the picture against the top-left corner and
     stepped the cards down and right: a pile pushed sideways. Round the
     centre reads as a sequence rather than as a shadow that forgot to blur,
     and it keeps the tile symmetrical in a lattice that is scanned.
     IT COSTS MORE PICTURE, which is the trade and is why the offset went up
     rather than down: leaning gives up `offset × 2` on two sides, centred
     gives up `offset` on four. Fourteen is what makes the stack legible at
     168px without the thumbnail reading as cropped.
     AND THE FAN IS FULL AGAIN. Two and a half degrees reached 4.5px past the
     tile when the outermost card sat flush against the bottom-right corner
     and had nowhere to turn; concentric at fourteen leaves it four pixels of
     clearance on every side, and the measured overhang is 0.75px against a
     six-pixel gutter. */
  .pic-deck i:nth-child(1) { inset: var(--pdk-c1, 4px); opacity: .62;
    transform: rotate(-2.4deg); }
  .pic-deck i:nth-child(2) { inset: var(--pdk-c2, 8px); transform: rotate(1.3deg); }
  /* AND THE FACE IS A PRINT TOO, with the paper showing around it — which is
     what the photograph this was taken from actually has, and what makes the
     picture read as the top of a pile rather than as a window with a frame.
     THE DROP IS FIXED RATHER THAN DERIVED FROM THE OFFSET. It used to scale
     with it, so at three pixels it was sub-pixel and invisible: depth is not
     a function of how far the cards poke out, and a print lifted a millimetre
     off the one beneath it still casts a shadow. */
  /* THE FACE'S OWN PAPER IS AN ELEMENT NOW, not a ring. It was
     `box-shadow: 0 0 0 3px var(--deck-card)` — which can only ever be a flat
     colour, so the topmost sheet was the one thing in the pile that could not
     be lit. A spread shadow takes no gradient and `border-image` takes no
     border-radius, so the third sheet is drawn the way the other two are:
     a box behind the face, inset three pixels outside it.
     ON `.pic-deck` AND NOT ON `.pic-sq`, which has `overflow: hidden` and
     would have clipped it to nothing. `.pic-deck` is the span before the link
     in document order, so this paints under the face with no z-index and no
     stacking context to reason about — the same argument the note on
     `.pic-deck` already makes about the link.
     FOURTEEN, BEING ELEVEN AND THREE: the face inherits the tile's 11px and
     sits 3px inside this, and concentric corners want the outer radius to be
     the inner one plus the gap. */
  .pic-deck::after { content: ''; position: absolute; inset: var(--pdk-drop, 11px);
    border-radius: 14px; background: var(--deck-paper);
    box-shadow: 1px 2px 6px -1px rgba(0, 0, 0, .8); }
  /* ONLY THE DECK, and that is the whole scope of it. An ordinary tile's
     empty state is the lattice's own dark surface and stays that way: there
     is no paper around it to be continuous with, and a white rectangle in a
     wall of dark ones would read as a picture rather than as a gap.
     IT SHOWS WHILE LOADING AND NOT AFTER. The `<img>` covers this the moment
     it arrives, so this is the back of the print rather than a tint over the
     picture — the one place it stays visible is behind a face that never
     loads, which is exactly where a blank sheet is the honest drawing. */
  .pic.is-deck .pic-sq { inset: var(--pdk-mat, 14px); background: var(--deck-face); }
  /* Opened, so the tile says which band below it belongs to it. */
  .pic.is-deck.is-open { border-color: var(--pool-lit);
    box-shadow: 0 0 0 1px var(--pool-lit), 0 0 14px 1px var(--pool-haze); }
  /* HOW MANY, ON HOVER, in the corner the duplicate pill just left. Quiet on
     purpose: the stack already says "more than one" to anybody scanning, and
     the number is for the moment somebody stops on it. Inset nine pixels so
     it sits on the FACE rather than across the slivers. */
  /* ON THE PICTURE, NOT ON THE PAPER. Both of this tile's corner pills were
     placed to clear a six-pixel sliver; the face is inset fourteen now, so
     nine put them on the cards. A deck is the only tile where that is true,
     so the shift is scoped to one and the ordinary tile is untouched. */
  .pic.is-deck .pic-dup { left: var(--pdk-pin, 18px); bottom: var(--pdk-pin, 18px); }
  .pic-deck-n { position: absolute; right: 9px; bottom: 9px; z-index: 7;
    padding: 1px 5px; border-radius: 5px; pointer-events: none;
    font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: .04em;
    color: var(--text-2); border: 1px solid var(--glass-line);
    background: color-mix(in srgb, var(--panel) 88%, transparent);
    opacity: 0; transition: opacity .12s ease; }
  .pic.is-deck .pic-deck-n { right: var(--pdk-pin, 18px); bottom: var(--pdk-pin, 18px); }
  .pic:hover .pic-deck-n, .pic:focus-within .pic-deck-n { opacity: 1; }

  /* ── AND OPENED IN PLACE ────────────────────────────────────────────────
     ITS OWN GRID, WITH THE LATTICE'S OWN COLUMNS, so a member reads as a tile
     and not as a thumbnail in a strip. That is the whole of why this is cheap:
     every verb, tick, pill and chip a tile already has keeps working, and
     there is no second set of rules to learn or to maintain.
     FULL WIDTH AFTER THE LAST TILE OF THE DECK'S ROW, for the reason
     `.pic-open` is placed the same way — inserting directly after the tile
     leaves the rest of its row as a hole. */
  .pool-band { grid-column: 1 / -1; display: grid; gap: 6px;
    grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
    /* Wider than it looks it needs to be, and that IS the reason: the slot in
       front of the first tile and the one behind the last are aimed at in
       this padding. */
    padding: 10px 14px; border-radius: 12px; background: var(--surface-2);
    border: 1px solid var(--glass-line);
    box-shadow: inset 0 0 0 1px var(--pool-haze); }
  .pool-band-note { grid-column: 1 / -1; padding: 14px 4px; text-align: center;
    color: var(--text-3); font-size: 13px; }
  .pic.is-pooled:hover { border-color: var(--pool-lit);
    box-shadow: 0 0 0 1px var(--pool-lit), 0 0 12px 1px var(--pool-haze); }
  /* The anchor is the face and nothing more: the tick and the action strip
     are siblings, so neither is inside the link and neither navigates. */
  .pic-link { position: absolute; inset: 0; border-radius: inherit;
    display: block; color: inherit; text-decoration: none; }
  /* ── A TILE WITH NOTHING IN IT YET ──────────────────────────────────────
     A hole with a filename in it, which is what an unloaded `<img>` draws,
     reads as a fault; a card waiting for a picture reads as a card waiting
     for a picture. So the crop carries a gradient of its own and the image
     fades in over it.
     THE DECK'S OWN PAPER, mixed down hard. A tenth of `--deck-card` over the
     surfaces is enough to tie the empty tile to the stacks beside it without
     a hundred of them flashing pale across a dark page on every load. */
  .pic-sq { position: absolute; inset: 0; overflow: hidden; border-radius: inherit;
    background:
      linear-gradient(155deg,
        color-mix(in srgb, var(--deck-card) 11%, var(--surface-3)) 0%,
        color-mix(in srgb, var(--deck-card) 4%, var(--surface-2)) 52%,
        var(--surface) 100%); }
  /* FADED IN, which is also what hides the alt text: an `<img>` that has not
     arrived draws its `alt`, and the attribute has to stay for a reader that
     cannot see the picture at all. Transparent until it loads says both. */
  .pic-sq img { width: 100%; height: 100%; object-fit: cover; display: block;
                opacity: 0; transition: opacity .18s ease; }
  .pic-sq img.is-in { opacity: 1; }
  .pic:hover .pic-sq img.is-in { opacity: 0; }
  .pic-none { position: absolute; inset: 0; display: grid; place-items: center;
              color: var(--text-3); font-size: 11px; }

  /* The whole picture, from the SAME file the tile is cropping. The poster is
     aspect-fitted rather than square-cropped precisely so this costs no second
     request: `cover` above crops it to the lattice, `contain` here gives it
     back. Grown to 1.4 tiles and never past its own pixels — blowing a 90px
     reaction face up to 300 is not showing it properly, it is showing it
     worse. */
  /* A square made of PADDING, which is the one construction here that cannot
     be argued with. Two simpler spellings were tried and measured first:
     `height: 140%` gave the box no height at all, because a percentage height
     resolves only against a definite containing height and this tile's height
     comes from its own aspect-ratio; `aspect-ratio: 1` then let the picture's
     own stated height win over the ratio, so a 600x1500 screenshot rendered
     1500px tall out of a 219px box and covered the grid.
     `padding-top` resolves against the containing block's WIDTH, always, and
     the width here is a plain percentage of a real width. So the outer box is
     reliably square, and `.pic-pop-in` — abspos with `inset: 0` — turns that
     into a definite height for the picture to be capped against. */
  .pic-pop { position: absolute; left: 50%; top: 50%; z-index: 5;
             width: 140%; height: 0; padding-top: 140%;
             opacity: 0; pointer-events: none;
             transform: translate(-50%, -50%) scale(.94);
             transition: opacity .13s ease, transform .13s cubic-bezier(.2,.9,.3,1.1); }
  .pic-pop-in { position: absolute; inset: 0;
                display: flex; align-items: center; justify-content: center; }
  .pic:hover .pic-pop, .pic.is-lit .pic-pop { opacity: 1;
             transform: translate(-50%, -50%) scale(1); }
  /* No stated size at all, and that is the answer rather than a shortcut. An
     unsized replaced element takes its INTRINSIC size and the two maxes shrink
     it while preserving the ratio — which is exactly "as large as it really
     is, never larger than the box". The poster is written with `decrease`, so
     it is never upscaled and its intrinsic size IS the picture's own: a 96px
     reaction face therefore renders at 96px inside a 216px box.

     Two sized versions were tried and measured first. Stating the width and
     height in custom properties looks equivalent and is not — a replaced
     element with BOTH specified stops preserving its ratio, so the maxes
     squash it. And the percentage maxes only resolve at all because the
     wrapper is absolutely positioned with `inset: 0`; inside a grid with an
     auto row they resolved against a track the picture itself was sizing,
     which is circular, and a 600x1500 screenshot came out 1500px tall over
     the top of the grid. */
  .pic-pop img { display: block; border-radius: 9px;
                 max-width: 100%; max-height: 100%;
                 box-shadow: 0 14px 34px -10px rgba(0,0,0,.62),
                             0 0 0 1px color-mix(in srgb, var(--surface-3) 70%, transparent); }
  /* The one on top wins the pointer, so a grid of these does not flicker as
     the cursor crosses a neighbour's grown edge. */
  .pic:hover { z-index: 6; }
  /* AND SHIFT-CLICKING A TILE PICKS PICTURES, NOT WORDS. Shift-click is the
     browser's own "extend the text selection" gesture, so holding it across a
     grid leaves every caption highlighted blue behind the pictures you meant
     to choose. `preventDefault` on the click cannot undo it -- the selection
     happened on mousedown. The tile is the only thing turned off: `.pic-open`
     is a sibling, so the description and the transcript of a picture somebody
     opened are still text you can take. */
  .pic { user-select: none; -webkit-user-select: none; }

  /* ── THE SENATE ──────────────────────────────────────────────────────────
     A proposal is a card with one sentence, a reason, and a yes/no beside it.
     The vote is the loudest thing on the row because it is the only thing
     anybody comes here to do. */
  .sen-list { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
  .sen-row { display: grid; grid-template-columns: auto 1fr auto; gap: 14px;
    align-items: center; padding: 13px 15px; border-radius: 12px;
    background: var(--surface-2); border: 1px solid var(--surface-3); }
  /* A closed one stays on the board for a week and says so by receding. */
  .sen-row.is-shut { opacity: 0.62; }
  .sen-says { font-weight: 650; font-size: 0.92rem; }
  .sen-note { margin-top: 3px; color: var(--text-2); font-size: 0.8rem;
    line-height: 1.5; }
  .sen-by { margin-top: 4px; color: var(--text-3); font-size: 0.72rem;
    font-family: var(--font-mono); }
  /* ── THE BALLOT ───────────────────────────────────────────────────────
     TWO CHEVRONS AND TWO NUMBERS, stacked so the whole thing is one target
     the eye lands on rather than four. The colours are not decoration: green
     and red are the only two answers, and a tally in one colour would need
     reading to be understood.
     CLICKING THE LIT ONE TAKES THE VOTE BACK, which is why the lit state has
     to be unmistakable — otherwise the second press reads as a second vote. */
  .sen-ballot { display: grid; grid-template-columns: auto auto auto auto;
    align-items: center; gap: 6px; }
  .sen-arrow { display: grid; place-items: center; width: 26px; height: 26px;
    padding: 0; border: 0; border-radius: 7px; background: none; cursor: pointer;
    color: var(--text-3); transition: color .12s ease, background .12s ease; }
  .sen-arrow svg { display: block; width: 17px; height: 17px; }
  .sen-arrow:hover { background: var(--surface-3); color: var(--text); }
  .sen-arrow.up.is-on { color: var(--good, #30a46c);
    background: color-mix(in srgb, var(--good, #30a46c) 15%, transparent); }
  .sen-arrow.down.is-on { color: var(--tl-not);
    background: color-mix(in srgb, var(--tl-not) 15%, transparent); }
  .sen-arrow:disabled { opacity: .4; cursor: default; background: none; }
  .sen-tally { font-family: var(--font-mono); font-size: 0.78rem;
    letter-spacing: .02em; }
  .sen-tally .y { color: var(--good, #30a46c); }
  .sen-tally .n { color: var(--tl-not); }
  .sen-tally .dot { color: var(--text-3); margin: 0 4px; }
  .sen-left { margin-left: 10px; font-family: var(--font-mono);
    font-size: 0.68rem; color: var(--text-3); white-space: nowrap; }
  /* The verdict, where the clock was. Same slot, because a closed proposal
     answers the same question the countdown did: what is happening to this. */
  .sen-verdict { font-family: var(--font-mono); font-size: 0.6rem;
    letter-spacing: .04em; text-transform: uppercase; padding: 2px 7px;
    border-radius: 999px; background: var(--surface-3); color: var(--text-2); }
  .sen-verdict.passed { color: var(--good, #30a46c);
    background: color-mix(in srgb, var(--good, #30a46c) 14%, transparent); }
  .sen-verdict.rejected { color: var(--tl-not);
    background: color-mix(in srgb, var(--tl-not) 14%, transparent); }
  /* ── THE SOVEREIGN'S ROW ──────────────────────────────────────────────
     Under the card rather than beside the ballot, because these are not
     votes and a row that put them on the same line would be inviting the
     owner to press a verdict where everybody else presses an opinion. */
  .sen-acts { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px;
    flex-wrap: wrap; margin-top: 4px; padding-top: 10px;
    border-top: 1px solid var(--surface-3); }
  .sen-acts .ops-sub { margin-right: 2px; }
  .sen-acts .switch { margin-left: 0; }
  .sen-form { display: flex; flex-direction: column; gap: 10px; margin-top: 14px;
    padding: 15px; border-radius: 12px; background: var(--surface-2);
    border: 1px solid var(--surface-3); }
  .sen-form label { display: flex; flex-direction: column; gap: 5px;
    font-size: 0.76rem; color: var(--text-2); }
  .sen-form input, .sen-form textarea, .sen-form select {
    padding: 7px 11px; border-radius: 9px; border: 1px solid var(--border);
    background: var(--surface); color: var(--text); font: inherit;
    font-size: 0.82rem; }
  .sen-form textarea { resize: vertical; min-height: 64px; }
  .sen-form-acts { display: flex; gap: 8px; align-items: center; }
  .sen-say { color: var(--tl-not); font-size: 0.78rem; }

  /* ── the buttons ──────────────────────────────────────────────────────
     Children of the TILE, not of the thing that grows. That is the whole
     specification: the picture expands underneath them and they stay exactly
     where they were, so the second meme you deal with needs no aiming. */
  .pic-acts { position: absolute; right: 5px; top: 5px; z-index: 8;
              display: flex; gap: 3px; opacity: 0;
              transition: opacity .12s ease; }
  /* AND IT STAYS GATED, tried the other way and reverted. The rule for the
     rest of the family is that operations show at rest, because copy and
     download belong to whoever is looking. This strip is the exception and
     the reason is the surface, not the verbs: it is an OVERLAY on somebody's
     artwork, not a row beside it. Four icons on every tile in a grid is the
     grid reading as a control panel instead of as pictures. */
  .pic:hover .pic-acts, .pic:focus-within .pic-acts { opacity: 1; }
  /* THE ONE THING THIS SURFACE STILL OWNS, and it is not a style — it is
     that these sit ON a photograph. `.btn-ent` is transparent, which is right
     in a card's footer and unreadable over somebody's artwork, so the backing
     and the blur stay. Everything else — box, icon size, border, hover,
     the red — comes from the base. */
  .pic-acts .btn-ent { background: color-mix(in srgb, var(--panel) 88%, transparent);
    backdrop-filter: blur(7px); color: var(--text); }
  .pic-acts .btn-ent:hover { background: var(--panel); }

  /* ── the tick ──────────────────────────────────────────────────────────
     OPPOSITE CORNER FROM THE VERBS, because it is not one of them. The strip
     on the right acts on this picture now; the tick on the left says this
     picture is part of what the next verb acts on. Two different questions,
     and a hand that has learned one corner should not have to read icons to
     find the other.
     HOVER-GATED UNTIL THERE IS A SELECTION, which is the rule the action
     strip already follows and for its reason -- a tick on every tile is the
     grid reading as a form. Once ONE is ticked every tick stays up, because
     at that point the question on screen is which ones, and hunting for a
     control you have already used once is the worse of the two. */
  .pic-tick { position: absolute; left: 5px; top: 5px; z-index: 9;
    width: 22px; height: 22px; display: grid; place-items: center; padding: 0;
    border-radius: 7px; cursor: pointer; opacity: 0;
    border: 1px solid color-mix(in srgb, var(--text-3) 60%, transparent);
    background: color-mix(in srgb, var(--panel) 88%, transparent);
    backdrop-filter: blur(7px); color: var(--text-3);
    transition: opacity .12s ease, background .12s ease, color .12s ease,
                border-color .12s ease; }
  .pic-tick svg { display: block; width: 13px; height: 13px;
    transform: scale(0); transition: transform .12s ease; }
  /* A TARGET BIGGER THAN THE BOX. 22px is the right size for the box to LOOK
     and the wrong size for a pointer to have to land on, which is the whole
     of why picking several pictures feels fussy -- it is a precision motion
     repeated forty times. The pad is invisible, costs nothing, and takes the
     corner rather than the picture: it reaches a little further than the box
     into a part of a tile nobody aims at to open it. */
  .pic-tick::before { content: ''; position: absolute; inset: -9px; }
  .pic:hover .pic-tick, .pic:focus-within .pic-tick,
  .pics.has-sel .pic-tick { opacity: 1; }
  .pic-tick:hover { color: var(--text); border-color: var(--text-3); }
  .pic-tick[aria-pressed="true"] { background: var(--accent); color: var(--on-accent);
    border-color: transparent; opacity: 1; }
  .pic-tick[aria-pressed="true"] svg { transform: scale(1); }
  /* THE TILE SAYS SO TOO, because at forty tiles a 22px corner is not enough
     to read the selection off. A ring rather than a tint: a tint over
     somebody's artwork changes the artwork. */
  .pic.is-sel { box-shadow: 0 0 0 2px var(--accent); border-radius: 12px; }

  /* ── the bar ───────────────────────────────────────────────────────────
     THE CAPTURE BAR'S SHAPE, not the theatre sheet's. The first version was a
     full-width panel pinned to the bottom edge, which is the language this
     page uses for something you have opened and are now inside -- and this is
     neither. It is a small thing that appears because you picked some
     pictures and goes when you stop, which is exactly what `.cap` is: a
     floating card, centred, hovering off the bottom edge, with a mono label
     saying what it is. Same radius, same shadow, same hairline.

     z-index 46: under `.cap` (89) rather than beside it. Both are
     bottom-centred and both can in principle be up at once -- pressing `n` on
     the Gallery with a selection -- and the one you just summoned with a key
     should be the one on top. */
  /* ── one picture, on its own ────────────────────────────────────────────
     THE BOORU SHAPE, and it is a shape rather than a style: the picture as
     large as it will go, the words for it down one side, and the things you
     can DO with it above rather than floating over the artwork. The gallery's
     own strip hovers over a tile because a tile is a thumbnail in a lattice;
     here the picture is the page and putting controls on top of it would be
     covering the thing somebody came for. */
  .mo-top { display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
    padding: 0 0 12px; border-bottom: 1px solid var(--border); margin-bottom: 14px; }
  .mo-top .mo-back { margin-right: auto; }
  .mo-body { display: grid; gap: 18px; align-items: start;
    grid-template-columns: minmax(0, 1fr) 240px; }
  .mo-pic { min-width: 0; display: flex; justify-content: center; }
  /* NEVER LARGER THAN ITS OWN PIXELS, the same rule the hover preview
     follows: blowing a 90px reaction face up to fill a screen is not showing
     it properly, it is showing it wrong and loudly. */
  .mo-pic img, .mo-pic video { max-width: 100%; max-height: calc(var(--vh) * 76);
    border-radius: 12px; display: block;
    box-shadow: 0 18px 50px -14px rgba(0,0,0,.6); }
  .mo-side { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
  .mo-h { font-family: var(--font-mono); font-size: 0.56rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--text-3); }
  .mo-title { font-family: var(--font-display); font-weight: 700; font-size: 1.02rem;
    letter-spacing: -0.01em; line-height: 1.3; word-break: break-word; }
  .mo-title.is-none { color: var(--text-3); font-style: italic; font-weight: 500; }
  /* ONE PER LINE, which is the whole request and the right answer: a column
     of tags is read down rather than scanned across, and every one of them is
     a destination. Wrapped chips would be a filter row; this is a contents
     page. */
  .mo-tags { display: flex; flex-direction: column; gap: 1px; }
  .mo-tag { display: flex; align-items: baseline; gap: 7px; width: 100%;
    padding: 4px 6px; border: 0; border-radius: 6px; background: none;
    color: var(--text-2); font: inherit; font-size: 0.8rem; text-align: left;
    cursor: pointer; transition: background .12s ease, color .12s ease; }
  .mo-tag:hover { background: var(--surface-2); color: var(--text); }
  .mo-tag i { font-style: normal; font-size: 0.56rem; font-family: var(--font-mono);
    text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-3);
    flex: none; }
  .mo-tag b { font-weight: 500; min-width: 0; word-break: break-word; }
  .mo-none { color: var(--text-3); font-size: 0.78rem; font-style: italic; }
  .mo-facts { display: flex; flex-direction: column; gap: 3px;
    font-size: 0.72rem; color: var(--text-3); }
  .mo-facts b { color: var(--text-2); font-weight: 600; }
  @media (max-width: 760px) {
    .mo-body { grid-template-columns: minmax(0, 1fr); }
  }

  /* ── AND IT SITS ABOVE THE LIST RATHER THAN ON IT ─────────────────────
     RAISING IT IS HALF THE FIX AND THE SMALLER HALF. `Load more` is the last
     thing in the panel, so a fixed box at the bottom of the viewport covers
     it however high it sits — moving the bar up just covers the row above
     instead. What actually clears it is giving the panel somewhere to scroll
     TO, which is the `body.is-picbar` rule below. Both, because the gap also
     wanted to be bigger. */
  .pic-bar { position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%)
      translateY(140%); z-index: 46;
    width: min(760px, calc(calc(var(--vw) * 100) - 28px)); box-sizing: border-box;
    background: var(--panel); border-radius: 12px; padding: 9px 11px 8px;
    box-shadow: 0 0 0 1px rgba(255,255,255,0.1), 0 20px 50px rgba(0,0,0,0.62);
    transition: transform .26s cubic-bezier(.22,1,.36,1); }
  .pic-bar.open { transform: translateX(-50%); }
  /* ROOM AT THE END OF WHATEVER THE BAR IS OVER, and on `body` because the
     bar is one element shared by every surface that has a selection, and on
     `main` rather than on the panels for the same reason: one visible panel
     at a time, so the room lands wherever the bar actually is, and the next
     surface to grow a selection gets it without being added to a list. The
     first version of this named two panels and one of them did not exist.
     The number is the
     bar's own height plus its gap plus a thumb's width of air; it is a
     scrolling allowance rather than a layout, so being approximate is fine
     and being absent is not. */
  body.is-picbar main { padding-bottom: 116px; }
  .pic-bar[hidden] { display: none; }
  .pic-bar-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .pic-bar-lab { flex: none; font-family: var(--font-mono); font-size: 0.56rem;
    letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-3); }
  .pic-bar-n { flex: none; font-family: var(--font-mono); font-size: 0.68rem;
    font-weight: 700; color: var(--accent); background: var(--accent-soft);
    padding: 4px 8px; border-radius: 6px; }
  .pic-bar-x { flex: none; background: transparent; border: 0; border-radius: 5px;
    padding: 2px 5px; color: var(--text-3); cursor: pointer; font-size: 0.8rem; }
  .pic-bar-x:hover { color: var(--text); background: var(--surface-2); }
  /* WHAT THEY HAVE IN COMMON, and only that. A list of every tag across forty
     pictures is forty pictures' tags, which is not a fact about the selection;
     the intersection is. Capped, because a selection of two pictures off one
     post shares everything they have and this is not a tag list. */
  .pic-bar-tags { display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
    min-width: 0; margin-top: 6px; }
  /* AND `+ tag` SITS IN IT, which is where it sits on a row. The first
     version of this was a text field in the bar and a glyph beside the other
     verbs -- two new shapes for a control the page already has, in the one
     place where it is most obviously the same act. `.btn-add.tl-add` is that
     control; nothing here restyles it. */
  .pic-bar-tags:empty { display: none; }
  .pic-bar-tags .tl { pointer-events: none; }
  .pic-bar-more { font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-3); }
  /* THE VERBS ARE `btn-ent`, which is the family for an operation on a thing
     the archive holds -- the same box the tile's own strip wears, because
     these are the same verbs over more than one picture. The two that are not
     operations on pictures (select all, clear) stay words. */
  .pic-bar-acts { display: flex; align-items: center; gap: 6px; flex: none;
    margin-left: auto; }
  .pic-bar-acts .btn-op { white-space: nowrap; }
  .pic-bar-sep { width: 1px; height: 20px; background: var(--border); flex: none;
    margin: 0 2px; }
  .pic-bar-say { font-size: 0.63rem; color: var(--text-3); margin-top: 6px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .pic-bar-say.bad { color: var(--tl-not); }
  #pic-bar-prog { margin-top: 7px; }
  #pic-bar-prog[hidden] { display: none; }
  @media (max-width: 680px) {
    .pic-bar-acts { margin-left: 0; width: 100%; justify-content: flex-end; }
  }

  /* Waiting and removed read the same as they do in the clip list — dashed
     and tinted — because they mean the same thing and a second visual
     language for one idea is one more thing to learn. */
  /* `border: 1px dashed`, never a bare `border-style: dashed`. The width has
     to be restated or it falls back to `medium` — three pixels — and a tile
     three pixels smaller than its neighbours moves every absolutely positioned
     thing inside it. That is measurably what happened: the control strip sat
     in one place on published tiles and three pixels in from it on removed
     ones, which is the exact defect the strip is pinned to the square to
     avoid. Yellow for waiting, red for out, from the same two tokens the clip
     list uses. */
  .pic.is-pending { border: 1px dashed color-mix(in srgb, var(--tl-meta) 55%, transparent);
    background: color-mix(in srgb, var(--tl-meta) 5%, var(--surface-2)); }
  .pic.is-removed { border: 1px dashed color-mix(in srgb, var(--tl-not) 48%, transparent);
    background: color-mix(in srgb, var(--tl-not) 5%, var(--surface-2)); }
  .pic.is-removed .pic-sq img, .pic.is-removed .pic-pop img { filter: grayscale(.55) opacity(.72); }
  .pic.is-working { opacity: .55; pointer-events: none; }

  /* A moving meme says so on the tile. Without it a gif and a still are the
     same rectangle until you click one, and the difference is the whole
     reason it was uploaded. */
  .pic-tag { position: absolute; left: 5px; bottom: 5px; z-index: 7;
             font-size: 9px; font-weight: 700; letter-spacing: .04em;
             text-transform: uppercase; padding: 2px 5px; border-radius: 5px;
             background: color-mix(in srgb, var(--panel) 86%, transparent);
             backdrop-filter: blur(7px); color: var(--text-3);
             border: 1px solid color-mix(in srgb, var(--surface-3) 70%, transparent); }

  /* ── the opened row ───────────────────────────────────────────────────
     Full width, inserted after the last tile of the row the click was in
     rather than immediately after the tile itself — which would leave the
     rest of that row as a hole. Same trick every image grid on the web uses,
     and it is worth the one line of arithmetic that finds the column count. */
  .pic-open { grid-column: 1 / -1; display: grid; gap: 16px;
              grid-template-columns: minmax(0, 1.35fr) minmax(230px, .65fr);
              padding: 14px; border-radius: 13px; background: var(--surface-2);
              border: 1px solid var(--surface-3); position: relative; }
  .pic-open-img { display: grid; place-items: center; min-width: 0;
                  min-height: 170px;
                  background: color-mix(in srgb, var(--bg) 55%, transparent);
                  border-radius: 10px; padding: 8px; }
  /* 55vh, not 70: the row has to fit its metadata beside it on one screen, or
     opening a picture becomes a scroll away from the thing you opened it to
     read. Big enough to look at, small enough to still be a row. */
  .pic-open-img img, .pic-open-img video {
    max-width: 100%; max-height: calc(var(--vh) * 55); width: auto; height: auto;
    display: block; border-radius: 7px; }
  .pic-open-side { display: flex; flex-direction: column; gap: 9px; min-width: 0; }
  /* ── the sequence, above the title ──────────────────────────────────────
     Where it was asked for, and the reason is the reading order: the strip
     answers "what is this one of" before the title answers "what is this",
     and a picture in a comic is the second question. Scrolls sideways rather
     than wrapping — a sequence is a line. */
  .pic-pool:empty { display: none; }
  /* The side padding is the drop zone in front of the first and behind the
     last — without it those two slots are a one-pixel target. */
  .pool-row { display: flex; gap: 6px; overflow-x: auto; padding: 1px 8px 5px;
    scrollbar-width: thin; }
  .pool-i { flex: none; width: 46px; height: 46px; padding: 0; cursor: pointer;
    border-radius: 7px; overflow: hidden; background: var(--surface-2);
    border: 1px solid var(--surface-3); opacity: 0.72; }
  .pool-i img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .pool-i:hover { opacity: 1; border-color: var(--border); }
  /* WHERE YOU ARE, which a strip of equals cannot say. Full opacity and the
     accent, because the one thing a reader needs off this row is which of
     these they are looking at. */
  .pool-i.is-here { opacity: 1; border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent); cursor: default; }
  /* ── WHERE IT WILL LAND ────────────────────────────────────────────────
     A BAR IN THE GAP, NOT A LIT NEIGHBOUR. The question a drag is asking is
     "between which two", and a highlighted thumbnail answers "which one" —
     which is the same ambiguity that made the two most wanted positions, in
     front of the first and behind the last, impossible to aim at: neither has
     a thumbnail to land on.
     ABSOLUTE, SO NOTHING REFLOWS. A real element in the flow would re-wrap the
     lattice under the cursor every time the slot changed, which is a grid that
     moves while you are aiming at it. */
  .pool-slot { position: absolute; z-index: 9; pointer-events: none;
    width: 3px; border-radius: 2px; background: var(--accent);
    box-shadow: 0 0 7px 0 var(--accent-soft); }
  /* AND A SPACE OPENING FOR IT, by transform: the two either side of the slot
     lean apart and everything else stays exactly where it was. Paint, not
     layout — which is the whole reason this can follow a pointer. */
  .pool-shove-l { transform: translateX(-5px); }
  .pool-shove-r { transform: translateX(5px); }
  .pool-band > .pic, .pool-row .pool-i { transition: transform .12s ease; }
  /* The thing being moved, while it is being moved. */
  .pool-lift { opacity: 0.4; }
  /* ── THE CONTAINER IS THE DROP TARGET, NOT THE TILES ────────────────────
     While a drag is in flight the items stop taking pointer events, which is
     not a tidiness: the slot is computed from COORDINATES, so the items never
     needed to be hit targets — and leaving them as targets cost the whole
     gesture. The tile either side of the slot leans apart by transform, and
     Chromium cancels a drop when the element it was aimed at moves out from
     under the pointer, so dropping onto a tile silently produced `dragend`
     and no `drop` at all. Dropping into the padding worked, which is what
     made it look like an aiming problem rather than a target one.
     The container does not move, so the drop always lands.
     NOT THE SOURCE, THOUGH. Taking pointer events off the element the drag
     STARTED on wedges the gesture in Chromium — it is the drag's own anchor,
     and the browser stops feeding the loop. So everything except the thing
     being carried. */
  .pool-dragging > .pic:not(.pool-lift),
  .pool-dragging .pool-i:not(.pool-lift) { pointer-events: none; }
  .pool-row, .pool-band { position: relative; }
  .pool-band > .pic { cursor: grab; }
  .pool-band > .pic.pool-lift { cursor: grabbing; }
  .pool-none { display: grid; place-items: center; width: 100%; height: 100%;
    font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-3); }
  .pool-bar { display: flex; align-items: center; gap: 8px; margin: 0 0 10px;
    font-size: 0.7rem; color: var(--text-3); }
  .pool-bar code { font-family: var(--font-mono); font-size: 0.66rem;
    color: var(--text-2); overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  .pool-n { flex: none; }
  /* The control, in the upload form and in the opened row. One class, because
     it is one gesture in two places: paste an id, or mint one. */
  .pool-set { display: flex; align-items: center; gap: 6px; margin: 0 0 10px; }
  .pool-set input { flex: 1 1 auto; min-width: 0; font: inherit; font-size: 0.74rem;
    font-family: var(--font-mono); padding: 6px 9px; border-radius: 8px;
    color: var(--text); background: var(--glass-sunk); border: 1px solid var(--border); }
  .pool-set input:focus { border-color: var(--accent); outline: none; }
  .pool-set .cs-btn { flex: none; }
  /* ---- the five most recent sequences -----------------------------------
     `.up-ac`'s well, to the pixel: a list under a field is already a thing
     this page draws, and a second recipe for it would be two lists that drift
     apart. What differs is the ROW, because a tag row is a word and a
     sequence row is a picture, a count and a time. */
  .pool-ac { margin-top: 5px; border: 1px solid var(--glass-line); border-radius: 9px;
    background: rgba(0,0,0,0.2); overflow: hidden; }
  .pool-ac-none { padding: 7px 9px; font-size: 0.7rem; color: var(--text-3); }
  .pool-ac-row { display: flex; align-items: center; gap: 8px; width: 100%;
    border: 0; background: none; color: var(--text); font: inherit;
    font-size: 0.72rem; padding: 5px 9px; text-align: left; cursor: pointer; }
  .pool-ac-row:hover, .pool-ac-row:focus-visible { outline: none;
    background: color-mix(in srgb, var(--accent) 20%, transparent); }
  /* THE ONE IT IS ALREADY IN, marked rather than hidden. Pressing it is a
     no-op and that is fine; not being able to see which one you are in is
     not, because the field shows a ULID and nobody reads those. */
  .pool-ac-row.is-here { box-shadow: inset 3px 0 0 var(--accent); }
  /* A FACE, because a pool has no name. 34x34 and cropped: the thumbnail is
     doing the identifying, so it has to be big enough to recognise and small
     enough that five of them are a list rather than a gallery. */
  .pool-ac-face { flex: 0 0 34px; width: 34px; height: 34px; border-radius: 5px;
    overflow: hidden; background: var(--glass-sunk); display: grid;
    place-items: center; }
  .pool-ac-face img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .pool-ac-face i { font-style: normal; font-size: 0.7rem; color: var(--text-3); }
  .pool-ac-n { flex: none; font-family: var(--font-mono); font-size: 0.66rem;
    color: var(--text-2); }
  .pool-ac-t { flex: 1 1 auto; min-width: 0; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .pool-ac-t.is-none { color: var(--text-3); font-style: italic; }
  .pool-ac-when { flex: none; font-size: 0.64rem; color: var(--text-3); }
  /* ── what the tagger said ───────────────────────────────────────────────
     One line, and absent whenever there is nothing on it: a permanent "not
     tagged" on every row of a deployment with no sidecar is a fault report
     nobody asked for. */
  .pic-au { display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
    font-size: 0.7rem; color: var(--text-3); }
  /* THE UPLOAD WINDOW'S BAR, not a second one. `.up-bar.indet` is already the
     answer this page gives to "something is happening and there is no honest
     percentage" -- a 34% sliver sweeping a hairline -- and the note beside
     `.only-you.working` says why a FAKE percentage would be worse than a word.
     An indeterminate sweep claims nothing it does not know.
     `1 1 100%` so it wraps onto its own line under the sentence: the row is a
     flex and the bar is the width of the panel, not a chip in it. */
  .pic-au .up-bar { flex: 1 1 100%; margin-top: 2px; }
  .au-busy { display: inline-flex; align-items: center; gap: 6px;
    color: var(--accent); }
  .pic-au .cs-btn { flex: none; }
  /* THE SECONDARY OF THE PAIR, and only by a shade. `tag again` and `untag`
     are the same act from two ends and either can be the one you want, so
     neither gets to look like the answer -- but the one that takes something
     away reads flatter until the pointer is on it, which is the same restraint
     the audit panel's own delete learned. */
  .pic-au .au-undo { background: transparent; color: var(--text-3); }
  .pic-au .au-undo:hover { color: var(--text-2); }
  .au-why.sad { color: var(--tl-not); }
  /* The rating. It was a STATEMENT and it is now also a CONTROL — a person
     may overrule the tagger, and `mlRatingTags` turns `explicit` into the tag
     that carries the gate. Mono, because it is one of four fixed words and
     reads as a value; the pill and the two colours are shared with
     `.rate-pick` below rather than restated, so the thing you press is
     visibly the thing you were reading. */
  .au-rate { font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: .02em;
    padding: 2px 6px; border-radius: 999px; text-transform: uppercase;
    background: var(--glass-sunk); border: 1px solid var(--border); color: var(--text-2); }
  /* FOUR RATINGS, THREE COLOURS, AND IT READS AS A SCALE. The first version
     coloured only the two that make somebody look twice and left `general` and
     `sensitive` bare, on the grounds that colouring every row teaches the eye
     to stop reading them. That holds for the middle of the scale and not for
     its ends: `safe` is the one answer somebody wants to confirm at a glance
     while scrolling a grid, and bare-means-safe is indistinguishable from
     bare-means-nobody-has-said. So green at the bottom, red at the top, amber
     between, and `sensitive` keeps no colour of its own — which is what stops
     this from being four colours nobody reads. */
  .au-rate.is-general { color: var(--good, #30a46c);
    border-color: color-mix(in srgb, var(--good, #30a46c) 40%, transparent); }
  .au-rate.is-questionable { color: var(--tl-meta);
    border-color: color-mix(in srgb, var(--tl-meta) 45%, transparent); }
  .au-rate.is-explicit { color: var(--tl-not);
    border-color: color-mix(in srgb, var(--tl-not) 55%, transparent); }
  /* A tag nobody has checked. A dashed underside and nothing else: it is still
     a tag, it still filters, and it is still the same chip — the only claim
     being made is that no human has looked at it. Marking it any harder would
     be the page arguing with its own vocabulary. */
  .tl.is-ml { border-bottom-style: dashed;
    border-bottom-color: color-mix(in srgb, var(--text-3) 70%, transparent); }
  /* The offer to add a record the archive has none of. Sits above the cards
     rather than among them, because it is not one of them: a card is what the
     archive holds and this is a thing you may ask for. */
  .au-add { margin: 0 6px 8px 0; }
  /* The verbs on this panel, in the face the rest of the archive uses for a
     value. Capitalising them was the first answer and it was half of one:
     `Delete` beside `Fetch` beside `Rebuild` still reads as three sentences.
     Set in the mono face and upper-cased, they read as the same KIND of
     thing — which is what they are. */
  /* ALL THAT IS LEFT OF IT. This began as five classes sharing a face;
     `au-del`, `au-ref` and `au-pick` went to `.btn-op`, which is that face
     with a name, and `au-add` has gone to `.btn-add`. `au-ask1` is the last
     one and nobody has looked at it yet — when somebody does, this rule goes
     with it. */
  .au-ask1 { font-family: var(--font-mono); text-transform: uppercase;
    letter-spacing: 0.06em; font-size: 0.66rem; }
  /* DELETE IS A WORD, NOT A BUTTON. The chrome around it was doing the one
     thing chrome should not do here — making the irreversible act look like
     the ordinary ones beside it. Stripped to red text, it reads as the odd
     one out in a row of controls, which is what it is.

     That argument is now `.btn-op` and `.btn-op.op-danger`, which is where it
     always belonged: it is a claim about a KIND of control, not about this
     panel. What stood here — `background: none`, `border-color: transparent`,
     the red, the hover underline, and then two more rules putting the colour
     BACK for `Keep` and for `Rebuild` — was six declarations of undoing.
     `op-danger` is applied by the same condition that chooses the word, so
     `Keep` and `Rebuild` simply never get it. Nothing left to cancel.
     `.au-del` survives as a hook for the handler and for `.au-doomed`. */
  .au-del { padding: 4px 6px; }
  /* The new-entry form. A card like the records it will sit beside, so the
     page reads as one surface rather than as a form bolted onto a report. */
  .au-make { display: grid; gap: 10px; max-width: 460px; }
  .au-make .tp-fld { margin: 0; }
  .au-mhint { font-size: 0.7rem; line-height: 1.55; color: var(--text-3); }
  .au-mbar { display: flex; gap: 8px; align-items: center; }
  /* THE RECORDS ON A NEW ENTRY. A plain `.au-rec` nested one level in, so the
     cards read as the same object the audit draws rather than as a second
     kind of form -- but every box on them is live, not a report with a
     double-click behind it, so they are built out of `.tp-fld` like the entry
     boxes above them and carry none of the `.au-in` readonly machinery. */
  .au-moffer { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
  .au-mrecs { display: grid; gap: 10px; }
  /* A CARD INSIDE A FORM, and the form is itself an `.au-rec`. Two nested
     borders read as two boxes, which is one more than there is -- so the
     record keeps the shape and loses the outline, and a tinted face says it
     is a thing inside the thing rather than beside it. */
  .au-mrec { display: grid; gap: 8px; border: 0;
    background: var(--glass-line); padding: 9px 11px; }
  .au-mrec .au-rech { margin-bottom: 0; }
  .au-mgrow { flex: 1; }
  /* The id and its fetch glyph are one reading, so they sit on one line and
     the button lines up with the box rather than with the label above it. */
  .au-mrow { display: flex; gap: 6px; align-items: flex-end; }
  .au-mrow .tp-fld { flex: 1; min-width: 0; }
  /* The fields that agree, folded. Quiet by design — it is a receipt, not a
     finding, and the things worth reading are drawn above it. */
  .au-agree > summary { cursor: pointer; list-style: none; padding: 5px 0;
    font-size: 0.7rem; color: var(--text-3); }
  .au-agree > summary::-webkit-details-marker { display: none; }
  .au-agree > summary::before { content: '\25B8'; display: inline-block;
    width: 1em; transition: transform .12s ease; }
  .au-agree[open] > summary::before { transform: rotate(90deg); }
  .au-agree > summary:hover { color: var(--text-2); }
  /* The merged file's sources, as the glyphs the cards above already use. */
  .au-src { display: inline-flex; align-items: center; gap: 6px; }
  .au-srci { display: grid; place-items: center; color: var(--text-2); }
  .au-srci svg { display: block; width: 14px; height: 14px; }
  .pic-open-t { font-size: 15px; font-weight: 650; line-height: 1.3; word-break: break-word; }
  /* An absent description reads as absent. Blank would leave the row looking
     unfinished, and a filename turned into a title would read as a name
     somebody chose — which is the thing this replaced. */
  .snip-title.is-none, .pic-open-t.is-none { color: var(--text-3); font-weight: 500;
    font-style: italic; }
  .up-opt { font-weight: 400; color: var(--text-3); margin-left: 4px; }
  /* Which Discord channels a picture may be taken from. Said before somebody
     pastes a link, not after — a rule you only learn by breaking it is a rule
     people stop trying to follow. */
  /* ---- API keys ----------------------------------------------------------
     The upload window's chrome, docked centre-screen rather than under a
     button: there is no button to dock under, because this opens from the
     username menu. */
  /* CENTRED WITHOUT A TRANSFORM. `.up-win.open` sets `transform: none` to land
     its entry animation, so a `translateX(-50%)` here would be undone the
     moment the window opened and it would hang off to the right. Auto margins
     between two zero insets centre a fixed box with nothing to override. */
  .tok-win { left: 0; right: 0; margin: 0 auto; top: calc(var(--vh) * 8);
    width: min(560px, calc(calc(var(--vw) * 100) - 32px));
    max-height: calc(var(--vh) * 84); transform-origin: top center; }
  .tok-form { display: flex; flex-direction: column; gap: 9px; }
  /* STRETCH, so the field and the button beside it are one height whatever
     either of their paddings works out to — matching two boxes by arithmetic
     is matching them until somebody changes a font size. */
  .tok-row { display: flex; align-items: stretch; gap: 8px; }
  /* THE FIELDS, WHICH HAD NONE. `.up-win input[type=text]` carries the glass
     and the radius and deliberately no padding — every row that uses it adds
     its own — so this one inherited a box the height of its text and the bevel
     cut the placeholder in half. `input[type=number]` is not even in that
     selector list, so the two number fields were raw browser chrome sitting
     next to styled ones. Both, here, once. */
  .tok-win input[type="text"], .tok-win input[type="number"] {
    background: var(--glass-sunk); border: 1px solid var(--glass-line);
    color: var(--text); font-family: inherit; font-size: 0.79rem;
    padding: 7px 11px; border-radius: 9px; }
  .tok-win input[type="text"] { flex: 1 1 auto; min-width: 0; }
  /* NO SPINNERS. Two numbers typed once each do not want a pair of arrows for
     stepping through them, and the native ones are the browser's own chrome —
     they ignore the field's colours and are the one part of this window that
     still looked borrowed. `appearance` covers Firefox, the pseudo-elements
     cover WebKit, and neither is reachable by the other. */
  .tok-win input[type="number"] { width: 5em; flex: none; text-align: center; }
  /* ONE RULE, TWO FIELDS. `#ops-cr-default` had the arrows too, and a second
     copy of this is how one of them keeps them the next time somebody edits
     the other. The width and the centring above stay with the token window,
     which is the only part of it that was ever about that window. */
  .tok-win input[type="number"], #ops-cr-default {
    appearance: textfield; -moz-appearance: textfield; }
  .tok-win input[type="number"]::-webkit-outer-spin-button,
  .tok-win input[type="number"]::-webkit-inner-spin-button,
  #ops-cr-default::-webkit-outer-spin-button,
  #ops-cr-default::-webkit-inner-spin-button {
    -webkit-appearance: none; appearance: none; margin: 0; }
  /* THE TWO VERBS, EACH WITH A PICTURE OF WHAT IT DOES TO A BALANCE. Mint
     withdraws and Deposit pays back, and on screen the only difference between
     them was the word. `inline-flex` because `.cs-btn` is a text button and a
     glyph dropped into one sits on the baseline. */
  .tok-row > .cs-btn { flex: none; display: inline-flex; align-items: center;
    gap: 7px; }
  .tok-row > .cs-btn svg { width: 15px; height: 15px; flex: none; }
  /* ONE FACT WITH TWO NUMBERS IN IT, so it reads as a sentence rather than as
     two labelled boxes. */
  .tok-says { flex-wrap: wrap; align-items: center; }
  .tok-says span { color: var(--text-3); font-size: 0.79rem; }
  #tok-cost { margin: 0; }
  /* THE SCOPE, grouped by the part of each name before the dot — derived and
     not hand-listed, for the reason the role editor gives: every hand-written
     list of capabilities in this project has eventually disagreed with the
     real one. */
  /* AND IT DOES NOT SCROLL INSIDE A SCROLLER. This had a `28vh` ceiling and
     its own scrollbar, which was right while a permission was a chip: thirty
     of them in two words each wanted bounding. A permission is now a row with
     a sentence in it, and the same ceiling cut the third one off mid-line
     behind a scrollbar nobody saw, inside a panel that already scrolls. The
     bound it was defending — a window that outgrows the screen — is `.tok-win`'s
     `max-height` and `.up-body`'s overflow, both of which were already there
     and neither of which it was doing anything for. */
  .tok-caps { display: flex; flex-direction: column; gap: 10px; padding: 2px; }
  .tok-grp { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
  .tok-grp > b { flex: 0 0 5.5rem; font-family: var(--font-mono);
    font-size: 0.58rem; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-3); font-weight: 500; }
  .tok-cap { display: inline-flex; align-items: center; gap: 5px;
    padding: 3px 7px; border-radius: 7px; background: var(--glass-sunk);
    border: 1px solid transparent; cursor: pointer; }
  .tok-cap code { font-family: var(--font-mono); font-size: 0.6rem;
    color: var(--text-2); }
  .tok-cap:has(input:checked) { border-color: var(--accent); }
  .tok-cap:has(input:checked) code { color: var(--text); }
  /* A BOX THIS WINDOW DREW. The native checkbox is the browser's own chrome:
     it ignores the accent, it is a different size and shape on every engine,
     and it was the last thing in here that still looked borrowed. The input is
     still the input -- it keeps the label, the focus ring and the change event;
     only its paint is replaced, which is the difference between restyling a
     control and rebuilding one. */
  .tok-perm input[type="checkbox"], .tok-cap input[type="checkbox"] {
    appearance: none; -webkit-appearance: none; margin: 0; flex: none;
    width: 15px; height: 15px; border-radius: 4px; cursor: pointer;
    display: grid; place-items: center;
    background: var(--glass-sunk); border: 1px solid var(--glass-line);
    transition: background 0.12s ease, border-color 0.12s ease; }
  /* THE TICK IS TWO BORDERS TURNED, not a glyph: a character would bring a
     font's own metrics into a 15px box and sit differently in each one. */
  .tok-perm input[type="checkbox"]::after,
  .tok-cap input[type="checkbox"]::after {
    content: ''; width: 8px; height: 4px; margin-top: -2px;
    border: 2px solid var(--on-accent); border-top: 0; border-right: 0;
    transform: rotate(-45deg) scale(0); transition: transform 0.12s ease; }
  .tok-perm input[type="checkbox"]:checked,
  .tok-cap input[type="checkbox"]:checked {
    background: var(--accent); border-color: transparent; }
  .tok-perm input[type="checkbox"]:checked::after,
  .tok-cap input[type="checkbox"]:checked::after {
    transform: rotate(-45deg) scale(1); }
  .tok-perm input[type="checkbox"]:focus-visible,
  .tok-cap input[type="checkbox"]:focus-visible {
    outline: 2px solid var(--accent); outline-offset: 2px; }

  /* THE METERED ONES GET A SENTENCE EACH, which is the whole reason they are
     listed apart: `transcript.run` tells somebody nothing about what a run
     costs, and the number beside it is the only thing a budget is about. Two
     columns, the tick spanning both rows, so the name and its sentence share
     one left edge rather than stepping. */
  .tok-pgrp { display: flex; flex-direction: column; gap: 5px; }
  .tok-pgrp > b { font-family: var(--font-mono); font-size: 0.58rem;
    letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-3);
    font-weight: 500; }
  .tok-perm { display: grid; grid-template-columns: auto 1fr; gap: 2px 9px;
    align-items: start; padding: 7px 9px; border-radius: 9px; cursor: pointer;
    background: var(--glass-sunk); border: 1px solid transparent;
    transition: border-color 0.12s ease; }
  .tok-perm > input[type="checkbox"] { grid-row: 1 / span 2; margin-top: 2px; }
  .tok-perm code { font-family: var(--font-mono); font-size: 0.66rem;
    color: var(--text-2); }
  .tok-perm small { font-size: 0.71rem; color: var(--text-3); line-height: 1.45; }
  .tok-perm:has(input:checked) { border-color: var(--accent); }
  .tok-perm:has(input:checked) code { color: var(--text); }

  /* AND EVERYTHING ELSE, FOLDED. `review.decide` and `change.apply` are real
     powers a script may well want; they are just not what a budget counts, so
     they are one line away rather than thirty names deep in the way. */
  .tok-more > summary { cursor: pointer; list-style: none; padding: 4px 0;
    font-size: 0.72rem; color: var(--text-3); }
  .tok-more > summary::-webkit-details-marker { display: none; }
  .tok-more > summary:hover, .tok-more[open] > summary { color: var(--text-2); }
  .tok-more .tok-grp { margin-top: 5px; }
  .tok-acts { display: flex; align-items: center; gap: 10px; margin-top: 4px; }
  .tok-acts .ops-sub { margin-right: auto; }
  /* THE SECRET, ONCE. Only a hash is stored and no route can give it back, so
     the one place it will ever appear says so and is selectable as one unit —
     the next thing anybody does with it is copy it. */
  .tok-secret { margin: 10px 0 2px; padding: 9px 11px; border-radius: 9px;
    background: var(--glass-sunk); border: 1px solid var(--accent);
    font-family: var(--font-mono); font-size: 0.68rem; color: var(--text);
    word-break: break-all; user-select: all; }
  .tok-why { margin: 6px 0 0; font-size: 0.72rem; color: var(--text-3);
    line-height: 1.5; }

  /* THE CHANNEL LIST, which was a paragraph under the limits and is now a
     popover on the words that name it. The list answers one question somebody
     asks once; a paragraph that is always there spends four lines of a small
     window on it every time the window opens. */
  .up-chanwrap { position: relative; display: inline; }
  .up-chanbtn { font: inherit; color: var(--text-2); background: none;
    border: 0; padding: 0; cursor: pointer; text-decoration: underline;
    text-decoration-style: dotted; text-underline-offset: 3px; }
  .up-chanbtn:hover, .up-chanbtn[aria-expanded="true"] { color: var(--accent); }
  /* ABOVE the word, because the limits line sits near the bottom of the window
     and a popover hanging below it would open off the end of the panel. */
  .up-chan { position: absolute; z-index: 60; left: 0; bottom: calc(100% + 6px);
    width: max-content; max-width: 19rem; padding: 8px 10px; border-radius: 8px;
    font-family: var(--font-ui); font-size: 0.71rem; line-height: 1.5;
    text-transform: none; letter-spacing: normal; color: var(--text-3);
    background: var(--panel); border: 1px solid var(--border);
    box-shadow: 0 10px 26px rgba(0,0,0,0.45); }
  .up-chan[hidden] { display: none; }
  /* The bridge across the 6px gap: a pointer travelling from the word to the
     list would otherwise leave the wrapper halfway and shut it. */
  .up-chan::before { content: ""; position: absolute; left: 0; right: 0;
    bottom: -8px; height: 8px; }
  .up-chan b { color: var(--text-2); font-weight: 600; }
  /* The pair a replacement makes, said on the row a verdict is given on. Not a
     `kind-pill`: those are facts ABOUT this picture and this is a fact about
     another one with a consequence attached — so it gets a line of its own,
     above the title, where it cannot be skimmed past. */
  /* The artist's road in, under the tags where the row's own facts end and
     what you may DO to it begins. Quiet: it is one person's affordance on a
     screen everybody else reads. */
  .snip-swap { display: flex; align-items: center; gap: 8px; margin: 6px 0 2px; }
  .snip-swap-note { font-size: 0.68rem; color: var(--text-3); }
  .snip-swap-note.is-bad { color: #ffb3b3; }

  .snip-sup { display: flex; align-items: center; gap: 8px; margin: 0 0 7px;
    padding: 5px 7px; border-radius: 8px; background: var(--surface-2);
    border: 1px solid var(--glass-line); font-size: 0.7rem; color: var(--text-3); }
  .snip-sup img { width: 34px; height: 34px; flex: none; border-radius: 5px;
    object-fit: cover; background: var(--surface-3); }
  .snip-sup span { display: flex; flex-direction: column; min-width: 0; line-height: 1.35; }
  .snip-sup b { color: var(--text-2); font-weight: 600; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .snip-sup a { margin-left: auto; flex: none; color: var(--accent); }

  .snip-from { position: absolute; left: 6px; bottom: 6px; z-index: 4;
    pointer-events: none; }
  /* The tagger's verbs, at the END of the action strip and held off it.
     Full size now that they are in that row -- a smaller button beside the
     others would read as a lesser version of them, and these are not that.
     What they are is a different OBJECT: the strip acts on the picture, these
     act on the tagger's reading of it, and the gap is the whole of what says
     so. Margin rather than a separator, because a rule between two groups of
     icons is a line the eye has to decide the meaning of. */
  .pic-acts-tail { display: inline-flex; gap: 6px; margin-left: 12px; }

  .pic-open-t[data-rvtitle] { cursor: text; }
  /* The three measurements: values on top, labels under them, one column
     each. `--n` is set per picture because a still has no duration and the
     grid must not leave a gap where the third column would be. */
  /* The word BESIDE the grid rather than over it, which is `from` one line
     down wearing the same clothes. It costs the width of the longest label in
     the panel -- accepted, because two rows that look like one list are worth
     more than the column it eats. */
  .pic-meta { margin: 2px 0 0; display: flex; align-items: baseline; gap: 8px;
    flex-wrap: wrap; min-width: 0; font-size: 11.5px; }
  /* EACH MEASUREMENT IS ITS OWN LITTLE COLUMN, which is a change of mechanism
     and not of appearance -- at any width where the row fits, this draws
     exactly what two grid rows drew.
     It was a 2 x n grid, and a grid of `auto` columns full of nowrap text has
     one width and cannot be talked out of it. The side column is
     `minmax(230px, .65fr)`, the label plus three measurements plus a rating
     pill is about 410px, and the difference went straight through the side of
     the panel. Cells that are each their own box WRAP instead, so a narrow
     panel costs a second line rather than a pill hanging in the margin.
     The row-major hazard goes with it: a value and its label are now siblings
     in one box, so neither can slide out from under the other. */
  .pic-meta-g { display: flex; flex-wrap: wrap; gap: 0 16px; min-width: 0; }
  .pic-meta-g .m { display: flex; flex-direction: column; }
  .pic-meta-g .v { font-size: 12px; color: var(--text); white-space: nowrap; }
  .pic-meta-g .k { font-size: 11px; color: var(--text-3); white-space: nowrap; }
  /* One line, always. A URL is as long as somebody else decided, and letting
     it wrap would move every control under it by a row. */
  .pic-where { display: flex; align-items: baseline; gap: 8px; min-width: 0;
    font-size: 11.5px; }
  .pic-where b, .pic-meta > b { font-weight: 600; color: var(--text-3);
    opacity: 0.8; flex: none; }
  /* The rating is a measurement cell now, and two defaults under it would
     undo that. A flex item STRETCHES across the cross axis, which on a
     bordered pill in a column box means a lozenge as tall as the cell; and
     nothing has told it not to break, so `questionable 94%` wrapped to two
     lines and took the row from 17px to 36px -- a measurements row whose
     height depends on which of four words the tagger picked, which is the one
     thing the order of this panel exists to prevent. */
  .pic-meta-g .au-rate { align-self: flex-start; white-space: nowrap; }
  .pic-where a, .pic-where span { min-width: 0; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  .pic-where a { color: var(--accent); }
  /* Shut by default, and the marker is the browser's. */
  .pic-tr summary { cursor: pointer; font-size: 0.68rem; color: var(--text-3);
    letter-spacing: 0.04em; padding: 2px 0; }
  .pic-tr summary:hover { color: var(--text-2); }
  .pic-tr[open] summary { margin-bottom: 4px; }

  .pic-open-meta { display: grid; grid-template-columns: auto 1fr; gap: 3px 10px;
                   font-size: 11.5px; color: var(--text-3); align-items: baseline; }
  .pic-open-meta b { font-weight: 600; color: var(--text-3); opacity: .8; }
  .pic-open-meta span { color: var(--text); word-break: break-word; }
  .pic-open-x { position: absolute; right: 9px; top: 9px; width: 26px; height: 26px;
                display: grid; place-items: center; border-radius: 7px;
                border: 1px solid var(--surface-3); background: var(--panel);
                color: var(--text-3); cursor: pointer; padding: 0; }
  .pic-open-x:hover { color: var(--text); border-color: var(--accent); }
  .pic-open .snip-tr { max-height: 190px; }
  /* `margin-top: auto` is gone with the reorder: it pinned this strip to the
     bottom of the column, which is exactly what made it move between one
     picture and the next. It sits where it is written now, above everything
     whose height depends on the picture. */
  .pic-open-acts { display: flex; gap: 6px; flex-wrap: wrap; }

  @media (max-width: 700px) {
    .pics, .pool-band {
      grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); }
    .pic-open { grid-template-columns: 1fr; }
    /* No hover on a touch screen, so the grown overlay is a thing that can
       only ever be triggered by the tap that is also opening the row. */
    .pic-pop { display: none; }
  }

  /* ── last word on [hidden] ────────────────────────────────────────────
     An author `display` outranks the UA stylesheet's [hidden] rule, so any
     element with one silently ignores the attribute. That has now cost four
     bugs in this file — a ghost block painted over the chapter it became, a
     no-duration message that detached the whole timeline, an empty tag slot,
     and a clock list that would not close. The half-dozen per-element
     `[hidden] { display: none }` rules above are each a scar from one of
     those; this line means there will not be a fifth. */
  [hidden] { display: none !important; }

